Когда реальность не соответствует ожиданиям — это всегда неприятно. Особенно неприятно, если за ожидаемую реальность заплачены деньги: покупаешь билет одной авиакомпании, но по какой-то причине тебя сажают в самолет совсем другой авиакомпании. Затем делаешь пересадку, а тебя там сажают в самолет какой-то непонятной третьей авиакомпании. Что происходит? Ведь на руках билет авиакомпании, который приобретен как раз потому, что знаешь, чего от нее ожидать, но летишь все равно самолетами совсем других авиакомпаний. Вроде бы покупал билет у крупной и хорошо себя зарекомендовавшей авиакомпании, а уже на борту выясняется, что у этой авиакомпании есть региональные дочерние компании и филиалы, которые далеко не так хороши. Первое знакомство с код-шеринговыми рейсами может оказаться очень неприятным.

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

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

Распределение пассажиропотоков

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

  • A \to B — рейсы выполняются авиакомпанией A_{1};

  • B \to C — рейсы выполняются авиакомпаниями A_{2} и A_{3};

  • A \to B \to C — на первом сегменте рейсы выполняются A_{1}, на втором A_{2} и A_{3} — это и есть маршрут, который требует оценки оптимальности код-шер соглашения.

Пассажиропотоки можно представить в двух формах — назовем их H и D:

  • H — элементарный пассажиропоток между двумя городами;

  • D — составной пассажиропоток между двумя городами.

Например, на сегменте A \to B авиакомпания A_{1} может оценить (наблюдать, если он меньше пропускной способности) пассажиропоток D^{1}_{1}, который является суммой двух элементарных пассажиропотоков:

D^{(1)}_{1} = H_{1} + H_{3},

где верхний индекс в скобках соответствует номеру авиакомпании. В данном случае H_{1} — это пассажиропоток путешественников, заинтересованных в том, чтобы добраться из A в B, а H_{3} — это пассажиропоток путешественников, заинтересованных в том, чтобы добраться из A в C.

Для сегмента B \to C все немного интереснее:

D^{(2)}_{2} = \alpha^{(2)}_{2} H_{2} + \alpha^{(2)}_{3} \cdot H_{3}

и

D^{(3)}_{2} = \alpha^{(3)}_{2} H_{2} + \alpha^{(3)}_{3} \cdot H_{3},

где \alpha может интерпретироваться как доля авиакомпании от элементарного пассажиропотока. В данном случае авиакомпании A_{2} и A_{3} делят между собой пассажиропоток H_{2} — путешественники, следующие из B в C, и пассажиропоток H_{3} — путешественники, следующие из A в C и совершающие пересадку в аэропорте города B.

Параметр \alpha также может интерпретироваться как вероятность совершения покупки билета на рейс той или иной авиакомпании, поэтому:

\alpha^{(2)}_{2} + \alpha^{(3)}_{2} = 1

и

\alpha^{(2)}_{3} + \alpha^{(3)}_{3} = 1.

\alpha зависит от привлекательности рейсов и определяется функцией полезности U(R), где R — это вектор параметров рейса, исходя из которых путешественники принимают решение о покупке билета. Обычно в качестве функции полезности используются экспоненцированные функции, которые преобразуются в вероятности с помощью soft-max функций. Хороший ли это способ? Он распространен и прост. В то же время сигмовидные функции и функция распределения Дирихле гораздо лучше укладываются в парадигму голосования. Например, если бы проводился опрос путешественников, в котором нужно было бы оценить каждый параметр рейса по 10-тибальной шкале, то результаты обрабатывались бы именно с помощью бета и Дирихле распределений. Это сложнее, но дает больше результатов.

Влияние код-шер соглашений на привлекательность маршрутов

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

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

  • Бронировать авиабилеты на рейсы с удобными стыковками, выбирая наиболее подходящий вариант среди предложений разных авиакомпаний.

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

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

Естественно, код-шеринговые рейсы также могут быть непривлекательны по ряду причин:

  • несоответствующий уровень сервиса;

  • разные правила провоза багажа и ручной клади;

  • вероятность возникновения проблем с багажом при пересадке.

Повышение привлекательности код-шерингового рейса может увеличить пассажиропоток:

H^{'}_{3} = \beta H_{3}, \;\;\; \beta > 1.

Кажется, что суть задачи сводится именно к этому, но все немного сложнее и гораздо интереснее. Теоретически код-шеринговые рейсы могут увеличить общий объем пассажиропотока, но в куда большей мере на них просто увеличивается спрос.

Добавление код-шерингового рейса ставит путешественников из H_{3} перед выбором с тремя альтернативами:

  1. Улететь из A в C код-шеринговым рейсом с минимальными неудобствами.

  2. Улететь из A в C через B с удобной пересадкой на рейс авиакомпании A_{2}.

  3. Улететь из A в C через B с неудобной пересадкой на рейс авиакомпании A_{3}.

Допустим, что так и есть, тогда H_{3} можно представить в виде следующей суммы:

H_{3} = H^{(\mathrm{rem})}_{3} + H^{(\mathrm{ch})}_{3},

где H^{(\mathrm{ch})}_{3} — это путешественники, выбравшие код-шеринговый рейс, а H^{(\mathrm{rem})}_{3} — это путешественники, выбравшие маршрут с самостоятельной пересадкой. H_{3} тоже должен разделяться в какой-то пропорции:

H_{3} = H^{(\mathrm{rem})}_{3} + H^{(\mathrm{ch})}_{3} = (1 - \alpha^{(1)}_{3}) H_{3} + \alpha^{(1)}_{3} H_{3}.

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

D^{(1)}_{1} = H_{1} + \alpha^{(2)}_{3} (1 - \alpha^{(1)}_{3}) H_{3} + \alpha^{(3)}_{3} (1 - \alpha^{(1)}_{3}) H_{3} + \alpha^{(1)}_{3} H_{3}D^{(2)}_{2} = \alpha^{(2)}_{2} H_{2} + \alpha^{(2)}_{3} (1 - \alpha^{(1)}_{3}) H_{3}D^{(3)}_{2} = \alpha^{(3)}_{2} H_{2} + \alpha^{(3)}_{3} (1 - \alpha^{(1)}_{3}) H_{3}.

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

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

Розлив и захват

Мы получили выражение, описывающее пропорциональное изменение объемов пассажиропотоков между авиакомпаниями. Осталось придумать, как вычислять пропорции на основе \alpha. Это может показаться очень сложным — в этом случае всегда прибегают к простым приближениям. Мы сделаем два важных предположения:

  1. У нас есть данные.

  2. Мы можем создавать модели данных.

По сути мы просто говорим о том, что мы "умеем в ML (Machine Learning)".

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

  • H_{1} \sim \mathrm{Poisson}(\lambda_{1});

  • H_{2} \sim \mathrm{Poisson}(\lambda_{2});

  • H_{3} \sim \mathrm{Poisson}(\lambda_{3}).

Рассмотрим, как происходит розлив и захват пассажиропотоков на примере H_{3}, ведь этот пассажиропоток делится между тремя авиакомпаниями. Разный уровень привлекательности рейсов вызывает разное внимание в разных ценовых сегментах пассажиров. Например, код-шеринговый рейс будет самым привлекательным, но по этой же причине он будет самым дорогим. Значит, что им будут интересоваться те, кто готов отдать за билет больше обычного.

Пусть привлекательность определяется двумя факторами: код-шерингом и временем пересадки.

Есть какое-то оптимальное время пересадки — обозначим его t^{*}. Допустим, что t^{(2)} — время пересадки на рейс A_{2} значительно ближе к t^{*}, чем t^{(3)} — время пересадки на рейс A_{3}.

Теперь мы можем определить привлекательность (полезность двух рейсов) через простую колоколообразную функцию. Рассмотрим три варианта:

  • \Delta t^{*} = t^{*} - t = 0 — минимальное отличие от идеального времени пересадки.

  • \Delta t^{*} = t^{*} - t > 0 — необходимо торопиться.

  • \Delta t^{*} = t^{*} - t < 0 — необходимо ждать.

Выразим привлекательности через функции полезности: u_{2} = U(\Delta t_{2}) и u_{3} = U(\Delta t_{3}), где U может быть любой унимодальной функцией: в нашем случае U — это функция Гаусса.

Привлекательность рейсов A_{2} и A_{3} может быть отображена следующим образом:

Python
import numpy as np
from scipy.stats import norm, gamma, poisson, binom, uniform, bernoulli
import matplotlib.pyplot as plt
from pylab import rcParams
rcParams['figure.figsize'] = 7, 4
rcParams['figure.dpi'] = 140
%config InlineBackend.figure_format = 'png'
import seaborn as sns
sns.set()
dt = np.linspace(-5, 5, 300)
u = norm.pdf(dt, loc=0, scale=1.4)

plt.plot(dt, u, 'C0')

dt_2 = 1.15
u_2 = norm.pdf(dt_2, loc=0, scale=1.4)
plt.plot(dt_2, u_2, 'C2o', label=r'$u_{2} =$' + f'{u_2:.2f}')
plt.vlines(dt_2, 0, u_2, color='C2')

dt_3 = 2.5
u_3 = norm.pdf(dt_3, loc=0, scale=1.4)
plt.plot(dt_3, u_3, 'C3o', label=r'$u_{3} =$' + f'{u_3:.2f}')
plt.vlines(dt_3, 0, u_3, color='C3')

plt.title('Attractiveness (usefulness) of flights\n' + r'of airlines $A_{2}$ and $A_{3}$')
plt.xlabel(r'$\Delta t$ (hour)')
plt.ylabel('u', rotation=0)
plt.legend()
plt.show()

Если авиакомпания A_{1} заключит код-шер соглашение с A_{2}, то, с точки зрения времени пересадки, привлекательность код-шерингового рейса будет такой же, как и у A_{2}, поскольку идеальное время пересадки - t^{*}, относительно которого рассчитываются все дельты, никак не поменялось. Однако, код-шеринговые рейсы дают целый ряд преимуществ: не нужно перерегистрировать багаж, заново проходить регистрацию, а для международных рейсов, оставаясь в чистой зоне, не придется проходить дополнительный паспортный контроль. Привлекательность код-шерингового рейса возрастает за счет того, что у путешественника снижаются риски на то, чтобы уложиться в \Delta t_{2}, а еще появляется дополнительное время на то, чтобы размять ноги и отдохнуть от полета. Выходит, что идеальное время пересадки для код-шерингового рейса — t^{*}_{ch}, все-таки отличается от идеального времени пересадки для обычного рейса.

Идеальное время для пересадки должно учитывать несколько факторов:

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

  • Дополнительное комфортное время — необходимо для снижения рисков бежать на регистрацию на другой рейс.

  • Среднее время опозданий рейсов — по каждой авиакомпании есть специальная статистика.

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

Если предположить, что t^{*}_{ch} < t^{*}, то это просто приведет к тому, что график функции полезности для код-шерингового рейса A_{1} сместится вправо:

Python
dt = np.linspace(-5, 5, 300)
u = norm.pdf(dt, loc=0, scale=1.4)

plt.plot(dt, u, 'C0', label=r'$t^{*} - t$')

dt = np.linspace(-5, 5, 300)
u_ch = norm.pdf(dt, loc=0.8, scale=1.4)
plt.plot(dt, u_ch, 'C0--', label=r'$t^{*}_{ch} - t$')

dt_1 = 1.15
u_1 = norm.pdf(dt_1, loc=0.8, scale=1.4)
plt.plot(dt_1, u_1, 'C1o', label=r'$u_{1} =$' + f'{u_1:.2f}')
plt.vlines(dt_1, 0, u_1, color='C1', lw=4, alpha=0.5)

dt_2 = 1.15
u_2 = norm.pdf(dt_2, loc=0, scale=1.4)
plt.plot(dt_2, u_2, 'C2o', label=r'$u_{2} =$' + f'{u_2:.2f}')
plt.vlines(dt_2, 0, u_2, color='C2')

dt_3 = 2.5
u_3 = norm.pdf(dt_3, loc=0, scale=1.4)
plt.plot(dt_3, u_3, 'C3o', label=r'$u_{3} =$' + f'{u_3:.2f}')
plt.vlines(dt_3, 0, u_3, color='C3')

plt.title('Attractiveness (usefulness) of flights\n' + r'of airlines $A_{2}$ and $A_{3}$')
plt.xlabel(r'$\Delta t$ (hour)')
plt.ylabel('u', rotation=0)
plt.legend()
plt.show()

Если раньше отличие от идеального времени пересадки составляло около 1 часа и 10 минут, то для код-шерингового рейса это отличие составит всего 20 минут. Естественно, приближение к идеальному времени пересадки — это не единственный фактор, влияющий на значение привлекательности (полезности) рейса. Время пересадки взято лишь в иллюстративных целях. В действительности вычисление привлекательности опирается на гораздо большее количество факторов и выполняется средствами ML.

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

D(\mathrm{Price}) = \mathrm{Gamma(Price, \mathrm{args})}
Python
prices_1 = np.linspace(100, 300, 1000)
rvdem_1 = gamma(a=12, loc=100, scale=8)
profile_1 = rvdem_1.pdf(prices_1)
plt.plot(prices_1, profile_1, label=r'$H_{1}$')

prices_2 = np.linspace(60, 200, 1000)
rvdem_2 = gamma(a=8, loc=60, scale=7)
profile_2 = rvdem_2.pdf(prices_2)
plt.plot(prices_2, profile_2, label=r'$H_{2}$')

prices_3 = np.linspace(240, 500, 1000)
rvdem_3 = gamma(a=10, loc=230, scale=12)
profile_3 = rvdem_3.pdf(prices_3)
plt.plot(prices_3, profile_3, label=r'$H_{1}$')

plt.legend()
plt.xlabel('Price (c.u.)')
plt.title('Passenger flow demand profiles');

Наличие профиля спроса говорит о том, что имеется надежное представление, при каких ценах вероятность покупки билета отлична от 0 или 1 и как она зависит от цены. Следует сразу отметить, что наличие таких ценовых профилей не позволяет рационально продемонстрировать старые или неоптимальные практики. Во-первых, после знакомства с методами ML довольно трудно представить, как можно что-то делать по-другому, тем более делать это хуже. Во-вторых, методик "прогнозирования" количества проданных билетов по некоторой цене как минимум несколько. В разных авиакомпаниях могут пользоваться разными из них. В некоторых, как правило, небольших авиакомпаниях могут вообще целиком и полностью полагаться на интуицию. Пока просто представим, что в худшем случае профили спроса представляли бы собой грубые ступенчатые функции.

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

D^{(1)}_{1} = H_{1} + \alpha^{(2)}_{3} (1 - \alpha^{(1)}_{3}) H_{3} + \alpha^{(3)}_{3} (1 - \alpha^{(1)}_{3}) H_{3} + \alpha^{(1)}_{3} H_{3}D^{(2)}_{2} = \alpha^{(2)}_{2} H_{2} + \alpha^{(2)}_{3} (1 - \alpha^{(1)}_{3}) H_{3}D^{(3)}_{2} = \alpha^{(3)}_{2} H_{2} + \alpha^{(3)}_{3} (1 - \alpha^{(1)}_{3}) H_{3}

Поток H_{1} между авиакомпаниями не делится. Делятся только потоки H_{2} и H_{3}, и деление зависит от привлекательности рейсов, которая влияет на вероятность выбора. Привлекательности могут выглядеть следующим образом:

Python
fig, ax = plt.subplots(1, 3, figsize=(12, 3))

dt = np.linspace(-5, 5, 300)
u = norm.pdf(dt, loc=0, scale=1.4)
ax[0].plot(dt, u, 'C0')

dt = np.linspace(-5, 5, 300)
u_ch = norm.pdf(dt, loc=0.8, scale=1.4)
ax[0].plot(dt, u_ch, 'C0--')
ax[1].plot(dt, u_ch, 'C0--')

dt_1 = 1.15
u_1 = norm.pdf(dt_1, loc=0.8, scale=1.4)
ax[0].plot(dt_1, u_1, 'C1o', label=r'$u_{1} =$' + f'{u_1:.2f}')
ax[0].vlines(dt_1, 0, u_1, color='C1', lw=4, alpha=0.5)


dt_2 = 1.15
u_2 = norm.pdf(dt_2, loc=0, scale=1.4)
ax[0].plot(dt_2, u_2, 'C2o', label=r'$u_{2} =$' + f'{u_2:.2f}')
ax[0].vlines(dt_2, 0, u_2, color='C2')

dt_3 = 2.5
u_3 = norm.pdf(dt_3, loc=0, scale=1.4)
ax[0].plot(dt_3, u_3, 'C3o', label=r'$u_{3} =$' + f'{u_3:.2f}')
ax[0].vlines(dt_3, 0, u_3, color='C3')

ax[0].legend(loc=3)
ax[0].set_title('Attractiveness for $H_3$\n(codeshare $A_{1}$ and $A_{2}$)')
ax[0].set_xlabel(r'$\Delta t$ (hour)')


ax[1].plot(dt, u, 'C0')


dt_1 = 2.5
u_1 = norm.pdf(dt_1, loc=0.8, scale=1.4)
ax[1].plot(dt_1, u_1, 'C1o', label=r'$u_{1} =$' + f'{u_1:.2f}')
ax[1].vlines(dt_1, 0, u_1, color='C1', lw=4, alpha=0.5)


dt_2 = 1.15
u_2 = norm.pdf(dt_2, loc=0, scale=1.4)
ax[1].plot(dt_2, u_2, 'C2o', label=r'$u_{2} =$' + f'{u_2:.2f}')
ax[1].vlines(dt_2, 0, u_2, color='C2')

dt_3 = 2.5
u_3 = norm.pdf(dt_3, loc=0, scale=1.4)
ax[1].plot(dt_3, u_3, 'C3o', label=r'$u_{3} =$' + f'{u_3:.2f}')
ax[1].vlines(dt_3, 0, u_3, color='C3')

ax[1].legend(loc=3)
ax[1].set_title('Attractiveness for $H_3$\n(codeshare $A_{1}$ and $A_{3}$)')
ax[1].set_xlabel(r'$\Delta t$ (hour)')

dt = np.linspace(-5, 5, 300)
u = norm.pdf(dt, loc=0, scale=2.6)

plt.plot(dt, u, 'C0')

dt_2 = 1.15
u_2 = norm.pdf(dt_2, loc=0, scale=2.6)
ax[2].plot(dt_2, u_2, 'C2o', label=r'$u_{2} =$' + f'{u_2:.2f}')
ax[2].vlines(dt_2, 0, u_2, color='C2')

dt_3 = 2.5
u_3 = norm.pdf(dt_3, loc=0, scale=2.6)
ax[2].plot(dt_3, u_3, 'C3o', label=r'$u_{3} =$' + f'{u_3:.2f}')
ax[2].vlines(dt_3, 0, u_3, color='C3')
ax[2].legend(loc=3)
ax[2].set_title('Attractiveness for $H_2$')
ax[2].set_xlabel(r'$\Delta t$ (hour)');

Самый простой способ преобразовать полезности в вероятности — это умножить каждое из значений на некоторый коэффициент так, чтобы результирующая сумма равнялась 1:

p_{i} = \frac{u_{i}}{\sum u_{i}}.

Например, если A_{1} заключит код-шер соглашение с A_{2}, то вероятности выбора того или иного рейса будут следующими:

Python
u = np.array([0.28, 0.20, 0.06])

print(u / u.sum())
[0.51851852 0.37037037 0.11111111]

  • P_{3}(\mathrm{Choice} = A_{1} | \mathrm{Seg} = 1) = 0.52;

  • P_{3}(\mathrm{Choice} = A_{2} | \mathrm{Seg} = 1) = 0.37;

  • P_{3}(\mathrm{Choice} = A_{3} | \mathrm{Seg} = 1) = 0.11.

Однако на выбор влияет не только предпочтение, но и количество средств, достаточное для его совершения. Зная профили спроса, можно определить вероятность покупки билета по той или иной цене. Пусть цены будут следующими:

  • \mathrm{Price_{1}^{(1)}} = 190

  • \mathrm{Price_{3}^{(1)}} = 370

  • \mathrm{Price_{2}^{(2)}} = 135

  • \mathrm{Price_{2}^{(3)}} = 115

Тогда вероятность покупки билета по некоторой цене можно определить следующим образом:

P(\mathrm{Buy}=1 | \mathrm{Price}) = \int_{\mathrm{Price}} ^{\infty} \varphi(\mathrm{Price})d\mathrm{Price}$$.

Например, вероятность P_{3}^{(1)}(\mathrm{Buy}=1 | \mathrm{Price}=370) того, что билет на код-шеринговый рейс будет куплен по цене 370 у.е., будет равна 0.27:

Python
rvdem_3 = gamma(a=10, loc=230, scale=12)
print(rvdem_3.sf(370))
0.2727250812333501

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

P_{3}(\mathrm{Choice} = A_{1}) = P_{3}(\mathrm{Choice} = A_{1}) \times P_{3}^{(1)}(\mathrm{Buy}=1 | \mathrm{Price}=370).

Итоговые вероятности выбора того или иного рейса для таких пассажиров будут равны:

  • P_{3}(\mathrm{Choice} = A_{1} | \mathrm{Seg} = 1) = 0.14;

  • P_{3}(\mathrm{Choice} = A_{2} | \mathrm{Seg} = 1) = 0.1;

  • P_{3}(\mathrm{Choice} = A_{3} | \mathrm{Seg} = 1) = 0.03.

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

Что произойдет с теми, кто не может позволить себе покупку такого билета? Если количество средств пассажира окажется в интервале:

[\mathrm{Price_{1}^{(1)}} + \mathrm{Price_{2}^{(2)}} , \mathrm{Price_{3}^{(1)}}) = [325, 370)

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

  • P_{3}(\mathrm{Choice} = A_{2} | \mathrm{Seg} = 2) = 0.77;

  • P_{3}(\mathrm{Choice} = A_{3} | \mathrm{Seg} = 2) = 0.23.

Python
u = np.array([0.20, 0.06])

print(u / u.sum())
[0.76923077 0.23076923]

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

P(\mathrm{Buy}=1 | 325 \leqslant \mathrm{Price} < 370) = \int_{325} ^{370} \varphi(\mathrm{Price})d\mathrm{Price} = 0.45.
Python
rvdem_3 = gamma(a=10, loc=230, scale=12)
print(rvdem_3.sf(325)- rvdem_3.sf(370))
0.4541835759677653

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

  • P_{3}(\mathrm{Choice} = A_{2} | \mathrm{Seg} = 2) = 0.35;

  • P_{3}(\mathrm{Choice} = A_{3} | \mathrm{Seg} = 2) = 0.1.

Путешественники, чье количество средств находится в диапазоне [305, 325), могут позволить себе только рейс A_{3}. Вероятность покупки билета по цене 305 для этого сегмента составит:

P_{3}(\mathrm{Choice} = A_{3} | \mathrm{Seg} = 3) = P(\mathrm{Buy}=1 | 305 \leqslant \mathrm{Price} < 325) = \int_{305} ^{325} \varphi(\mathrm{Price})d\mathrm{Price} = 0.17.
Python
rvdem_3 = gamma(a=10, loc=230, scale=12)
print(rvdem_3.sf(305)- rvdem_3.sf(325))
0.17088396696109864

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

P_{3}(\mathrm{Choice} = A_{i}) = \sum_{j} P_{3}(\mathrm{Choice} = A_{i} | \mathrm{Seg} = j).

Пассажиропоток H_{3} разделится между авиакомпаниями следующим образом:

  • P_{3}(\mathrm{Choice} = A_{1}) = 0.14;

  • P_{3}(\mathrm{Choice} = A_{2}) = 0.1 + 0.35 = 0.45;

  • P_{3}(\mathrm{Choice} = A_{3}) = 0.03 + 0.1 + 0.17 = 0.3.

Вероятность P(\mathrm{Buy}=0), что билет не будет куплен вообще, составляет 0.1, что в сумме с вышеперечисленными вероятностями выбора дает 1. Значит, что объем потенциально-возможного пассажиропотока учтен полностью.

Проделав то же самое для пассажиропотока H_{2}, получим следующее:

  • P_{2}(\mathrm{Choice} = A_{2}) = 0.1;

  • P_{2}(\mathrm{Choice} = A_{3}) = 0.38.

Пассажиропоток H_{1} хоть и не делится между авиакомпаниями, но из-за цены в 190 у.е. какая-то его доля будет разлита, поэтому можно все равно ввести вероятность выбора рейса P_{1}(\mathrm{Choice} = A_{1}), которая будет определяться ценой (вероятностью покупки билета по данной цене):

P_{1}(\mathrm{Choice} = A_{1}) = P(\mathrm{Buy}=1 | \mathrm{Price} = 190) = \int_{190} ^{\infty} \varphi_{1}(\mathrm{Price})d\mathrm{Price} = 0.55
Python
rvdem_1 = gamma(a=12, loc=100, scale=8)
print(rvdem_1.sf(190))
0.5494501693973257

Можно было бы расписать подробно все значения \alpha, но, поскольку мы уже получили конкретные значения долей, то запишем все в более короткой форме:

D^{(1)}_{1} = 0.55 \times H_{1} + 0.14 \times H_{3} + 0.45 \times H_{3} + 0.3 \times H_{3}D^{(2)}_{2} = 0.1 \times H_{2} + 0.45 \times H_{3}D^{(3)}_{2} = 0.38 \times H_{2} + 0.3 \times H_{3}

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

Более развитый подход заключается в использовании нескольких критериев привлекательности при выборе предмета. Каждый предмет — это маршрут, у которого есть время отправления и прибытия, а также пересадки, которые могут отличаться как продолжительностью, так и типом. Следовательно, выбор одного предмета меняет ценность всех остальных — как тех, что уже в рюкзаке, так и тех, что еще не упакованы. Значит, что все \alpha необходимо вычислять каждый раз после выбора предмета. Однако, домножив все H на соответствующие цены, можно получить вполне разумные оценки дохода, а самое главное — прикинуть, с кем и на каких условиях заключать соглашение.

Метод довольно грубый, но следует отметить, что он связывает предпочтения путешественников с перераспределением пассажиропотоков. Delta Airlines — одна из пионеров в области оптимизации код-шеринговых соглашений отчитывалась, что такой оценки совместных маршрутов обеспечивал до 50 миллионов долларов дополнительного ежегодного дохода. Это стало не только одним из самых выдающихся результатов для 2000-х, но еще и прямым доказательством важности предпочтений путешественников. Позднее Air Canada показала, что такой метод обеспечивает до 80% прироста еженедельного дохода по сравнению с наивным методом, учитывающим лишь влияние бренда.

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

Моделирование и оптимизация

Авиакомпании A_{1} необходимо найти не только лучшие цены, но и квоты: w_{3}^{(1)} — количество мест, зарезервированное под код-шеринг, и w_{1}^{(1)} — количество мест для тех, кто следует из A в B. Если обозначить через V емкость самолета, то для квот выполняется простое условие: w_{1}^{(1)} + w_{3}^{(1)} \leqslant V^{(1)} — так же, как и для количества проданных билетов q_{1}^{(1)} + q_{3}^{(2)} + q_{3}^{(3)} \leqslant w_{1}^{(1)} и q_{3}^{(1)} \leqslant w_{3}^{(1)}.

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

  • C_{\mathrm{fix}} — фиксированные расходы, например: оплата экипажей, аэропортовые сборы за стоянку, взлет и посадку, обеспечение авиационной безопасности.

  • C_{\mathrm{var}} — переменные расходы, которые зависят от количества пассажиров, например: сбор за предоставление аэровокзального комплекса, обработку багажа. Сюда бы следовало отнести и расходы на керосин, ведь его количество также зависит от количества пассажиров и багажа, но пока для простоты мы его отнесем к C_{\mathrm{fix}}. Пусть C_{\mathrm{var}} = c_{\mathrm{var}}Q, например, c_{\mathrm{var}} может быть равна 25 у.е., значение которого просто умножается на количество пассажиров - Q.

  • \mathrm{Price}_{\mathrm{ch}}^{(2)} — затраты на приобретение мест, купленных у оперирующего перевозчика для код-шерингового рейса.

Тогда средняя прибыль A_{1} будет следующей функцией:

E(\mathrm{Profit}_{1}) = E( \mathrm{Price}_{1}^{(1)}(q_{1}^{(1)} + q_{3}^{(2)} + q_{3}^{(3)}) + \mathrm{Price}_{\mathrm{ch}}^{(1)}q_{3}^{(1)} - c_{\mathrm{var}}^{(1)}(q_{1}^{(1)} + q_{3}^{(1)} + q_{3}^{(2)} + q_{3}^{(3)})) - C_{\mathrm{fix}}^{(1)} - \mathrm{Price}_{\mathrm{ch}}^{(2)}w_{3}^{(1)}.

Прибыль второй авиакомпании:

E(\mathrm{Profit}_{2}) = E( \mathrm{Price}_{2}^{(2)}(q_{2}^{(2)} + q_{3}^{(2)}) - c_{\mathrm{var}}^{(2)}(q_{2}^{(2)} + q_{3}^{(1)} + q_{3}^{(2)})) + \mathrm{Price}_{\mathrm{ch}}^{(2)}w_{3}^{(1)} - C_{\mathrm{fix}}^{(2)}.

Можно подумать, что, если A_{1} заключает соглашение с A_{2}, то до прибыли A_{3} не должно быть никакого дела. На самом деле у код-шеринга есть весьма интересное следствие — конкурентное давление. Поэтому запишем целевую функцию и для A_{3}:

E(\mathrm{Profit}_{3}) = E( \mathrm{Price}_{2}^{(3)}(q_{2}^{(3)} + q_{3}^{(3)}) - c_{\mathrm{var}}^{(3)}(q_{2}^{(3)} + q_{3}^{(3)})) - C_{\mathrm{fix}}^{(3)}.

Мы знаем, что H_{j} \sim \mathrm{Poisson(\lambda_{j})}. Зная P_{j}(\mathrm{Choice} = A_{i}), мы могли бы записать количество купленных билетов q_{j}^{(i)} как

q_{j}^{(i)} = \mathrm{Bin}(n= H_{j}, p=P_{j}(\mathrm{Choice} = A_{i}))$$,

а далее приблизить биномиальное распределение нормальным. Даже для небольших значений H_{j} — стандартная практика, однако путешественники из H_{1} и H_{3} представляют собой случайную смесь — нет такого, что сначала продаются билеты только для тех, кто из H_{1}, а потом остатки мест распродаются только для тех, кто из H_{3}. Следует также отметить, что, если аэропорты A и B разделены значительным количеством часовых поясов, то смесь H_{2} и H_{3} будет иметь разный состав, зависящий от времени. Например, если A_{3} значительно снизит цену в ночное время, то это снижение сначала могут увидеть пассажиры из H_{3}, потому что у них вместо ночи может быть день.

Важно помнить, что на первом месте всегда данные. Даже то, что мы записали H_{j} \sim \mathrm{Poisson(\lambda_{j})} — это чрезвычайно грубое приближение, взятое просто для примера. Вместо распределения Пуассона всегда находится очень сложное условное распределение, которое и получается на основе данных.

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

Прежде чем что-то моделировать, необходимо определиться с основными параметрами задачи.

Предположим, что маршрут A \to B \to C — это маршрут Владивосток-Москва-Сочи: \mathrm{VVO} \to \mathrm{SVO} \to \mathrm{AER}.

Пусть рейсы выполняются на следующих самолетах:

  • \mathrm{VVO} \to \mathrm{SVO} — выполняется авиакомпанией A_{1} на самолете A350 c вместимостью V = 325.

  • \mathrm{SVO} \to \mathrm{AER} — выполняется авиакомпанией A_{2} на самолете A320 c вместимостью V = 180.

  • \mathrm{SVO} \to \mathrm{AER} — выполняется авиакомпанией A_{3} на самолете Boeing 737 c вместимостью V = 190.

Фиксированные расходы будут следующими:

  • C_{\mathrm{fix}}^{(1)} — 48360 у.е.;

  • C_{\mathrm{fix}}^{(2)} — 10270 у.е.;

  • C_{\mathrm{fix}}^{(2)} — 9890 у.е.

Переменные расходы:

  • c_{\mathrm{var}}^{(1)} — 6.8 у.е. / пасс.

  • c_{\mathrm{var}}^{(2)} — 5.4 у.е. / пасс.

  • c_{\mathrm{var}}^{(2)} — 5.1 у.е. / пасс.

Предположим, что пассажиропотоки распределены следующим образом:

  • H_{1} \sim \mathrm{Poisson}(\lambda_{1} = 370);

  • H_{2} \sim \mathrm{Poisson}(\lambda_{2} = 350);

  • H_{3} \sim \mathrm{Poisson}(\lambda_{3} = 110).

Python
def prob_in_trafic(prices):
    price_1, price_2, price_3 = prices
    rvdem_1 = gamma(a=12, loc=100, scale=8)
    rvdem_2 = gamma(a=8, loc=60, scale=7)
    rvdem_3 = gamma(a=10, loc=230, scale=12)
    # Н_1
    # Вероятность покупки билета у А_1
    pb_11 = rvdem_1.sf(price_1)
    # Вероятность что билет не купят
    pb_10 = 1 - pb_11

    ph_1 = [pb_10, pb_11]
    
    # H_2
    # Вероятности выбора А_2 или А_3 на основе предпочтений:
    uh_2 = np.array([0.14, 0.1])
    pseg_2 = uh_2 / uh_2.sum()
    
    if price_2 > price_3:
        pbp_22 = rvdem_2.sf(price_2)
        pbp_23 = rvdem_2.sf(price_3) - rvdem_2.sf(price_2)
        # Вероятность покупки билета у А_2 с учетом предпочтений:
        pb_22 = pbp_22 * pseg_2[0]
        # Вероятность покупки билета у А_3 с учетом предпочтений:
        pb_23 = pbp_22 * pseg_2[1] + pbp_23
    else:
        pbp_22 = rvdem_2.sf(price_2)
        pbp_23 = rvdem_2.sf(price_3)
        pb_22 = pbp_22
        pb_23 = 0
    
    # Вероятность что билет не купят
    pb_20 = 1 - pb_22 - pb_23
    
    ph_2 = [pb_20, pb_22, pb_23]
    
    # H_3
    # Вероятности выбора А_2 или А_3 на основе предпочтений:
    uh_3 = np.array([0.2, 0.06])
    pseg_3 = uh_3 / uh_3.sum()
    
    
    if (price_1 + price_2) > (price_1 + price_3):
        pbp_32 = rvdem_3.sf(price_1 + price_2)
        pbp_33 = rvdem_3.sf(price_1 + price_3) - rvdem_3.sf(price_1 + price_2)
        # Вероятность покупки билета у А_2 с учетом предпочтений:
        pb_32 = pbp_32 * pseg_3[0]
        # Вероятность покупки билета у А_3 с учетом предпочтений:
        pb_33 = pbp_32 * pseg_3[1] + pbp_33
    else:
        pbp_32 = rvdem_3.sf(price_1 + price_2)
        pbp_33 = rvdem_3.sf(price_1 + price_3)
        # Вероятность покупки билета у А_2 с учетом предпочтений:
        pb_32 = pbp_32
        # Вероятность покупки билета у А_3 с учетом предпочтений:
        pb_33 = 0
    
    
    # Вероятность что билет не купят:
    pb_30 = 1 - pb_32 - pb_33

    ph_3 = [pb_30, pb_32, pb_33]
    
    return ph_1, ph_2, ph_3


def sales(P):
    h_1 = poisson.rvs(mu=370)
    h_2 = poisson.rvs(mu=350)
    h_3 = poisson.rvs(mu=110)
    
    h_all = np.sum([h_1, h_2, h_3])
    p_in_h = np.array([h_1, h_2, h_3]) / h_all
    
    v_1, v_2, v_3 = 325, 180, 190
    q_1, q_2, q_3 = 0, 0, 0
    for iter in range(h_all):
        pass_in_h = np.random.choice([1, 2, 3], p=p_in_h)
        if pass_in_h == 1:
            if np.random.choice([0, 1], p=P[0]):
                if q_1 < v_1:
                    q_1 += 1
        
        if pass_in_h == 2:
            a_i = np.random.choice([0, 2, 3], p=P[1])
            if a_i == 2:
                if q_2 < v_2:
                    q_2 += 1
            if a_i == 3:
                if q_3 < v_3:
                    q_3 += 1
        
        if pass_in_h == 3:
            a_i = np.random.choice([0, 2, 3], p=P[2])
            if a_i == 2:
                if q_2 < v_2:
                    q_2 += 1
                    if q_1 < v_1:
                        q_1 += 1
            if a_i == 3:
                if q_3 < v_3:
                    q_3 += 1
                    if q_1 < v_1:
                        q_1 += 1
    
    return q_1, q_2, q_3


def profit_a(prices, Q):
    profit_1 = Q[0] * prices[0] - Q[0] * 6.8 - 48360
    profit_2 = Q[1] * prices[1] - Q[1] * 5.4 - 10270
    profit_3 = Q[2] * prices[2] - Q[2] * 5.1 - 9890
    return profit_1, profit_2, profit_3

def e_profit(prices, n_iter):
    P = prob_in_trafic(prices)
    return np.mean([profit_a(prices, sales(P)) for _ in range(n_iter)], axis = 0)

Теперь можно приступить к моделированию и оптимизации. Возникают вопросы: что и как оптимизировать? Что и с чем сравнивать, чтобы убедиться в приросте прибыли?

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

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

  • \mathrm{Price}_{1}^{(1)} = 185;

  • \mathrm{Price}_{2}^{(2)} = 114;

  • \mathrm{Price}_{2}^{(3)} = 110.

Тогда распределения их прибылей будут следующими:

Python
prices = [185, 114, 110]

q_data = []
profit_data = []
P = prob_in_trafic(prices)

for i in range(1000):
    Q = sales(P)
    q_data.append(Q)
    profit_data.append(profit_a(prices, Q))

q_data = np.array(q_data)
profit_data = np.array(profit_data)
fig, ax = plt.subplots(1, 3, figsize=(12, 3.5))

sns.kdeplot(profit_data[:, 0], ax=ax[0])
e_pa_1 = np.mean(profit_data[:, 0])
ax[0].axvline(e_pa_1, color='C3', label=f'{e_pa_1:.0f} c.u.')
ax[0].legend()
ax[0].set_title(r'$A_{1}$')
ax[0].set_xlabel('Profit (c.u.)')

sns.kdeplot(profit_data[:, 1], ax=ax[1])
e_pa_2 = np.mean(profit_data[:, 1])
ax[1].axvline(e_pa_2, color='C3', label=f'{e_pa_2:.0f} c.u.')
ax[1].legend()
ax[1].set_title(r'$A_{2}$')
ax[1].set_xlabel('Profit (c.u.)')

sns.kdeplot(profit_data[:, 2], ax=ax[2])
e_pa_3 = np.mean(profit_data[:, 2])
ax[2].axvline(e_pa_3, color='C3', label=f'{e_pa_3:.0f} c.u.')
ax[2].legend(loc='upper left')
ax[2].set_title(r'$A_{3}$')
ax[2].set_xlabel('Profit (c.u.)')

plt.suptitle('Airline profit distribution')
plt.tight_layout();

Для авиакомпании A_{3} цена явно не является наилучшей. Однако сейчас нас интересует именно то, как изменится средняя прибыль у авиакомпаний A_{2} и A_{3}, если они заключат между собой код-шер соглашение.

Python
def prob_in_trafic_ch(prices):
    price_1, price_2, price_3, price_ch = prices[:-1]
    rvdem_1 = gamma(a=12, loc=100, scale=8)
    rvdem_2 = gamma(a=8, loc=60, scale=7)
    rvdem_3 = gamma(a=10, loc=230, scale=12)
    
    # Н_1
    # Вероятность покупки билета у А_1
    pb_11 = rvdem_1.sf(price_1)
    # Вероятность что билет не купят
    pb_10 = 1 - pb_11

    ph_1 = [pb_10, pb_11]
    
    # H_2
    # Вероятности выбора А_2 или А_3 на основе предпочтений:
    uh_2 = np.array([0.14, 0.1])
    pseg_2 = uh_2 / uh_2.sum()
    
    if price_2 > price_3:
        pbp_22 = rvdem_2.sf(price_2)
        pbp_23 = rvdem_2.sf(price_3) - rvdem_2.sf(price_2)
        # Вероятность покупки билета у А_2 с учетом предпочтений:
        pb_22 = pbp_22 * pseg_2[0]
        # Вероятность покупки билета у А_3 с учетом предпочтений:
        pb_23 = pbp_22 * pseg_2[1] + pbp_23
    else:
        pbp_22 = rvdem_2.sf(price_2)
        pbp_23 = rvdem_2.sf(price_3)
        pb_22 = pbp_22
        pb_23 = 0
    
    # Вероятность что билет не купят
    pb_20 = 1 - pb_22 - pb_23
    
    ph_2 = [pb_20, pb_22, pb_23]
    
    # H_3
    # Вероятности выбора A_1, А_2 или А_3 на основе предпочтений:
    uh_3_ch = np.array([0.28, 0.2, 0.06])
    pseg_3_ch = uh_3_ch / uh_3_ch.sum()
    # Вероятности выбора А_2 или А_3 на основе предпочтений для тех кто не летит кодшером:
    uh_3 = np.array([0.2, 0.06])
    pseg_3 = uh_3 / uh_3.sum()
    
    
    if price_ch > (price_1 + price_2) > (price_1 + price_3):
        pbp_31 = rvdem_3.sf(price_ch)
        pbp_32 = rvdem_3.sf(price_1 + price_2) - rvdem_3.sf(price_ch)
        pbp_33 = rvdem_3.sf(price_1 + price_3) - rvdem_3.sf(price_1 + price_2)
        # Вероятность покупки билета у А_2 с учетом предпочтений:
        pb_31 = pbp_31 * pseg_3_ch[0]
        # Вероятность покупки билета у А_2 с учетом предпочтений:
        pb_32 = pbp_31 * pseg_3_ch[1] + pbp_32 * pseg_3[0]
        # Вероятность покупки билета у А_3 с учетом предпочтений:
        pb_33 = pbp_31 * pseg_3_ch[2] + pbp_32 * pseg_3[1] + pbp_33
    else:
        # Вероятность покупки билета у А_1 с учетом предпочтений:
        pb_31 = 0
        # Вероятность покупки билета у А_2 с учетом предпочтений:
        pb_32 = 0
        # Вероятность покупки билета у А_3 с учетом предпочтений:
        pb_33 = 0
    
    
    # Вероятность что билет не купят:
    pb_30 = 1 - pb_31 - pb_32 - pb_33

    ph_3 = [pb_30, pb_31, pb_32, pb_33]
    
    return ph_1, ph_2, ph_3


def sales_ch(P, w_1):
    h_1 = poisson.rvs(mu=370)
    h_2 = poisson.rvs(mu=350)
    h_3 = poisson.rvs(mu=110)
    
    h_all = np.sum([h_1, h_2, h_3])
    p_in_h = np.array([h_1, h_2, h_3]) / h_all
    
    v_1, v_2, v_3 = 325, 180, 190
    q_1, q_1_ch, q_2, q_3 = 0, 0, 0, 0
    
    for iter in range(h_all):
        pass_in_h = np.random.choice([1, 2, 3], p=p_in_h)
        if pass_in_h == 1:
            if np.random.choice([0, 1], p=P[0]):
                if q_1 < v_1 - w_1:
                    q_1 += 1
        
        if pass_in_h == 2:
            a_i = np.random.choice([0, 2, 3], p=P[1])
            if a_i == 2:
                if q_2 < v_2 - w_1:
                    q_2 += 1
            if a_i == 3:
                if q_3 < v_3:
                    q_3 += 1
        
        if pass_in_h == 3:
            a_i = np.random.choice([0, 1, 2, 3], p=P[2])
            if a_i == 1:
                if q_1_ch < w_1:
                    q_1_ch += 1
            if a_i == 2:
                if q_2 < v_2 - w_1:
                    q_2 += 1
                    if q_1 < v_1 - w_1:
                        q_1 += 1
            if a_i == 3:
                if q_3 < v_3:
                    q_3 += 1
                    if q_1 < v_1 - w_1:
                        q_1 += 1
    
    return q_1, q_1_ch, q_2, q_3


def profit_a_ch(prices, Q, w_1):
    profit_1 = (Q[0] * prices[0] + Q[1] * prices[-2]) - (Q[0] + Q[1]) * 6.8 - w_1 * prices[-1] - 48360
    profit_2 = (Q[2] * prices[1] + w_1 * prices[-1]) - (Q[2] + Q[1]) * 5.4 - 10270
    profit_3 = Q[3] * prices[2] - Q[3] * 5.1 - 9890
    return profit_1, profit_2, profit_3


def e_profit_ch(prices, w_1, n_iter):
    P = prob_in_trafic_ch(prices)
    return np.mean([profit_a_ch(prices, sales_ch(P, w_1), w_1) for _ in range(n_iter)], axis = 0)

Взглянем на все значения прибыли A_{1} и A_{2}, которые одновременно больше нуля:

Python
PA_1 = []
PA_2 = []
PA_3 = []
PP = []
for i in range(10000):
    pra_1 = np.random.randint(150, 270)
    pra_2 = np.random.randint(111, 170)
    pra_3 = 110
    pra_1_ch = np.random.randint(pra_1 + pra_2 , 500)
    pay_ch = np.random.randint(pra_2, pra_1_ch)
    prices = pra_1, pra_2, pra_3, pra_1_ch, pay_ch
    w_1 = np.random.randint(0, 110)
    pa = e_profit_ch(prices, w_1, 1)
    if pa[0] > 0 and pa[1] > 0:
        PA_1.append(pa[0])
        PA_2.append(pa[1])
        PA_3.append(pa[2])
        PP.append([prices, w_1])
plt.plot(PA_2, PA_1, 'bo', ms=3, alpha=0.5)

x = np.linspace(0, 17500)
y_1 = -x + 20000
y_2 = -0.8 * x + 20000

plt.plot(x, y_1, 'C3', label='Indifference')
plt.plot(x, y_2, 'C2', label='Interest')

plt.xlabel(r'$A_{2}$ profit (c.u)')
plt.ylabel(r'$A_{1}$ profit (c.u)')
plt.title('Scatter of positive profit values ​​of two airlines')

plt.legend()

plt.show()

Множество точек ограничивается сверху некоторой выпуклой вверх и монотонно-убывающей функцией, которая содержит все точки эффективного по Парето множества. Если бы данное множество было симметричным, то в качестве оптимальной точки можно было бы взять точку множества, которая ближе всего к прямой, отсекающей равные отрезки от осей координат. Такой подход называется индифферентным. Эффективное множество практически никогда не бывает симметричным, поэтому участники заинтересованы в том, чтобы асимметрия обязательно учитывалась при принятии совместного решения. В этом случае в качестве оптимальной точки берется точка, ближайшая к прямой, расположенной над множеством, и параллельной прямой, проходящей через точки максимальной заинтересованности (0, \mathrm{Profit}_{1}^{*}) и (\mathrm{Profit}_{2}^{*}, 0).

Построение такого множества для задачи в стохастической форме всегда требует исследования и аппроксимации — ее невозможно построить аналитически. Пусть точки максимальной заинтересованности будут следующими: (0, 12000) и (17500, 0). В качестве прямой, параллельной другой прямой, проходящей через точки максимальной заинтересованности, возьмем прямую с уравнением y = -0.8x + 20000. Тогда оптимальные цены примут следующие значения:

  • \mathrm{Price}_{1}^{(1)} = 185;

  • \mathrm{Price}_{2}^{(2)} = 114;

  • \mathrm{Price}_{\mathrm{ch}}^{(1)} = 345;

  • \mathrm{Price}_{\mathrm{ch}}^{(2)} = 120;

  • w_{3}^{(1)} = 29.

После введения код-шерингового рейса распределения прибыли будут выглядеть так:

Python
prices_opt = [185, 114, 110, 345, 120]
w_opt = 29
P = prob_in_trafic_ch(prices_opt)

q_ch_data = []
profit_ch_data = []

for i in range(1000):
    Q = sales_ch(P, w_opt)
    q_ch_data.append(Q)
    profit_ch_data.append(profit_a_ch(prices_opt, Q, w_opt))

q_ch_data = np.array(q_ch_data)
profit_ch_data = np.array(profit_ch_data)
fig, ax = plt.subplots(1, 3, figsize=(12, 3.5))

sns.kdeplot(profit_ch_data[:, 0], ax=ax[0])
e_pa_ch_1 = np.mean(profit_ch_data[:, 0])
ax[0].axvline(e_pa_1, color='C3', ls='--', label=f'{e_pa_1:.0f} c.u.')
ax[0].axvline(e_pa_ch_1, color='C3', label=f'{e_pa_ch_1:.0f} c.u.')
ax[0].legend()
delta_1 = 100 * (e_pa_ch_1 - e_pa_1) / e_pa_1
ax[0].set_title(rf'$A_{1}$ (+{delta_1:.1f}%)')
ax[0].set_xlabel('Profit (c.u.)')

sns.kdeplot(profit_ch_data[:, 1], ax=ax[1])
e_pa_ch_2 = np.mean(profit_ch_data[:, 1])
ax[1].axvline(e_pa_2, color='C3', ls='--', label=f'{e_pa_2:.0f} c.u.')
ax[1].axvline(e_pa_ch_2, color='C3', label=f'{e_pa_ch_2:.0f} c.u.')
ax[1].legend()
delta_2 = 100 * (e_pa_ch_2 - e_pa_2) / e_pa_2
ax[1].set_title(rf'$A_{2}$ (+{delta_2:.1f}%)')
ax[1].set_xlabel('Profit (c.u.)')

sns.kdeplot(profit_ch_data[:, 2], ax=ax[2])
e_pa_ch_3 = np.mean(profit_ch_data[:, 2])
ax[2].axvline(e_pa_3, color='C3', ls='--', label=f'{e_pa_3:.0f} c.u.')
ax[2].axvline(e_pa_ch_3, color='C3', label=f'{e_pa_ch_3:.0f} c.u.')
ax[2].legend(loc='upper left')
delta_3 = 100 * (e_pa_3 - e_pa_ch_3) / e_pa_3
ax[2].set_title(rf'$A_{3}$ (-{delta_3:.1f}%)')
ax[2].set_xlabel('Profit (c.u.)')

plt.suptitle('Airline profit distribution (codeshare)')
plt.tight_layout();

Цифры говорят сами за себя.

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

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

Красным цветом показано, сколько теряют авиакомпании с которыми не заключили соглашение — видно, что в среднем проигрыш превосходит выигрыш авиакомпаний, создавших код-шеринговый рейс.

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

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

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

Кстати, в данной статье был рассмотрен лишь один наиболее распространенный тип код-шера free-sale — когда обе авиакомпании могут публиковать рейс другого перевозчика, как свой собственный с небольшими или вовсе отсутствующими ограничениями относительно количества мест, которые могут быть проданы (в пределах емкости самолета или установленных лимитов). Однако рассмотренный метод оптимизации пригоден и для других типов код-шера, например, block-space — когда количество мест под продажу маркетинговым перевозчиком является фиксированным. Также метод пригоден для оптимизации интерлайн-соглашений, когда одна авиакомпании просто признает перевозочные документы другой авиакомпании. Интерлайн также влияет на привлекательность, но требует меньших ограничений со стороны авиакомпании.

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

В заключение

Прирост прибыли в 5% только за счет код-шеринговых соглашений — неплохой результат. А можно больше? Конечно, да. Данную задачу можно рассматривать как расширение задачи назначения флота IFAM, основанную на моделировании состава пассажиров и принципе розлива/захвата пассажиропотоков. Код-шеринговый рейс можно представить как рейс, выполняемый неким виртуальным самолетом. Это означает, что для дальнейшей оптимизации можно использовать уже хорошо отработанные техники — например, оптимизировать на уровне динамического ценообразования (подклассов), а не усредненных и грубых значений. В данном случае прирост прибыли может быть еще больше.

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

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

Как было отмечено в начале статьи, решение задачи возможно только при наличии данных — точнее огромного количества данных. Так или иначе все сводится к моделированию дискретного выбора, который совершают путешественники. Некоторые авиакомпании, обладая большими объемами исторических данных, с этим неплохо справляются, но даже они не могут соперничать с GDS (глобальная система распределения). Связано это с тем, что авиакомпании обладают данными по пассажиропотокам только своих маршрутных сетей, в то время как GDS видит всю картину целиком и способна проанализировать гораздо большее множество как маршрутов, так и сценариев изменений пассажиропотоков. Однако, надо сказать, что это касается только GDS нового поколения. К сожалению, старые системы никогда не занимались подобными задачами или занимались ими условно.

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

Чтобы понять возможности GDS, зададимся простым вопросом: что было бы, если бы в рассмотренной задаче все рейсы выполнялись не тремя, а одной авиакомпанией? В таком случае можно было бы оптимизировать только среднюю сумму прибылей от всех рейсов. Получается обычная IFAM задача, а прирост суммарной прибыли становится намного больше, чем от введения код-шеринговых рейсов. Проблема заключается лишь в том, что рейсы выполняются разными авиакомпаниями. У централизованного управления гораздо больше преимуществ, но и трудностей тоже. Ввиду недостаточного количества данных авиакомпании не могут решать такие задачи, а GDS — могут.

Комментарии (4)


  1. Kazzman
    03.08.2024 10:35
    +1

    А какой сегодня процент прямых продаж и продаж через gds?

    Было очень интересно, сложно, нужно перечитать еще пару раз.


    1. AndreyKotlov Автор
      03.08.2024 10:35
      +1

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


  1. GunterVas
    03.08.2024 10:35
    +1

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


    1. AndreyKotlov Автор
      03.08.2024 10:35

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