This notebook works on a pricing model for airline tickets for an airline on the KEF-LHR route, using data sourced from Skyscanner (21 April 2025).
The methodology draws from Table 1 of the research paper DOI:10.1016/j.jmaa.2019.06.012, Talluri & van Ryzin (2004), and Mumbower et al. (2014).
The exponential demand model is particularly suited for price-sensitive markets, such as leisure travelers, where small price increases lead to significant drops in bookings due to an exponentially distributed willingness to pay.
The model achieves 2 key objectives: - Optimal Price Identification: Finds the price that maximizes revenue, sensitive to market price elasticity. - EMSRb Seat Protection: Allocates seats to higher fare classes to optimize total revenue, using the Expected Marginal Seat Revenue (EMSRb) method.
The final visualization combines the revenue curve, optimal price, and an EMSRb protection table, providing a comprehensive tool for airline revenue management.
library(ggplot2)
library(latex2exp)
library(scales)
library(ggtext)
The model uses real-world pricing data from Skyscanner for an airline KEF-LHR route, collected for 21 April 2025, departing on a particular time. The dataset includes 10 fare classes, reflecting typical airline pricing structures. To estimate demand, we assume conversion rates (probabilities of booking at each price) based on realistic market behavior, informed by Mumbower et al. (2014). These rates decrease as prices increase, capturing the price sensitivity of leisure travelers.
Price sensitivity, denoted \(\alpha\), is estimated using a log-linear regression model, as described in Talluri & van Ryzin (2004). The regression fits the logarithm of conversion rates against prices: \(\log(\text{conversion_rate}) = \text{intercept} - \alpha \cdot \text{prices}\).
The marginal value, \(\Delta v\), accounts for operational costs (e.g., fuel, crew) and no-show adjustments. Following the methodology from DOI:10.1016/j.jmaa.2019.06.012, we calculate: \(\Delta v = \frac{\min(\text{prices}) + \max(\text{prices})}{3}\).
This approximation balances the lowest and highest fares to estimate a per-seat cost baseline.
# Observed prices (Skyscanner, Icelandair KEF-LHR, 21 April 2025)
prices <- c(241, 246, 247, 252, 255, 258, 261, 270, 271, 294)
# Assumed conversion rates (realistic market behavior)
conversion_rate <- c(0.95, 0.90, 0.88, 0.80, 0.75, 0.70, 0.65, 0.50, 0.48, 0.20)
# Estimate alpha using log-linear regression
log_conversion <- log(conversion_rate)
model <- lm(log_conversion ~ prices)
alpha <- -coef(model)[2]
# Marginal value (Δv)
delta_v <- (min(prices) + max(prices)) / 3
The parameter \(\alpha\) quantifies how sensitive demand is to price changes. A higher \(\alpha\) indicates a steeper decline in booking probability, typical for leisure routes like KEF-LHR. The \(\Delta v\) value ensures revenue calculations reflect net profit per sale, critical for accurate pricing decisions.
The exponential demand model assumes that the probability of a customer willing to pay at least price \(p\) follows: \(Pr(P_t \geq p) = e^{-\alpha p}\).
This model is ideal for markets where demand drops rapidly with price increases, as seen in leisure travel. The expected revenue per seat offered is calculated as: \(\text{Expected Revenue} = Pr(P_t \geq p) \cdot (p - \Delta v)\), where \(p - \Delta v\) is the profit margin per sale.
The optimal price, \(p^*\), maximizes this revenue. By taking the derivative of the revenue function and setting it to zero, we derive: \(p^* = \Delta v + \frac{1}{\alpha}\).
We compute the revenue curve over a price range of 190 to 320 EUR, creating a smooth curve with 500 points. This range covers the observed fares and allows visualization of revenue behavior beyond the data.
p_min <- 190 # Minimum price (EUR)
p_max <- 320 # Maximum price (EUR)
p_points <- 500 # Number of points
# Price range
p <- seq(p_min, p_max, length.out = p_points)
# Probability: Pr(Pt ≥ p) = exp(-αp)
pr_ge_p <- exp(-alpha * p)
# Expected revenue
expected_revenue_total <- pr_ge_p * (p - delta_v)
# Optimal price
p_star <- delta_v + 1 / alpha
df <- data.frame(
Price = p,
ExpectedRevenueTotal = expected_revenue_total
)
The optimal price \(p^*\) is highly sensitive to \(\alpha\). A larger \(\alpha\) (more price-sensitive market) reduces \(p^*\), aligning with the need to offer lower prices to attract bookings. The revenue curve peaks at \(p^*\), providing a clear target for pricing strategies.
The Expected Marginal Seat Revenue (EMSRb) method, introduced by Belobaba (1989) and detailed in Talluri & van Ryzin (2004), optimizes seat allocation by reserving seats for higher fare classes to maximize total revenue. This is critical for airlines operating with fixed capacity, such as the Airbus A320 with 180 seats used on the KEF-LHR route.
The EMSRb protection level for fare class \(j\) (protecting seats for classes 1 to \(j\)) is calculated as: \(y_j = \mu_{1:j} + z_j \cdot \sigma_{1:j}\), where: - \(\mu_{1:j} = \sum_{i=1}^j \mu_i\) is the cumulative mean demand for classes 1 to \(j\). - \(\sigma_{1:j} = \sqrt{\sum_{i=1}^j \sigma_i^2}\) is the cumulative standard deviation. - \(z_j = \Phi^{-1}\left(1 - \frac{f_{j+1}}{f_j}\right)\) is the z-score, with \(\Phi^{-1}\) as the inverse normal CDF and \(f_j\) as the fare for class \(j\).
The fare ratio \(\frac{f_{j+1}}{f_j}\) compares the next lower fare to the current fare, determining how many seats to protect based on revenue trade-offs. The source for this methodology is Belobaba’s seminal work (1989) and its application in Talluri & van Ryzin (2004).
We assume: - Mean demand (\(\mu\)) and standard deviation (\(\sigma\)) for each fare class, reflecting booking variability. - Aircraft capacity of 180 seats. - Fares sorted in descending order (highest to lowest), as required for EMSRb.
The lowest fare class has no protection (\(y_n = 0\)), as it accepts all remaining demand.
# Sort fares for EMSRb
fare <- sort(prices, decreasing = TRUE)
# Assumed demand parameters
mu <- c(5, 7, 9, 10, 12, 14, 16, 18, 20, 22) # Mean demand
sigma <- c(2, 2.5, 2.5, 3, 3, 3.5, 3.5, 4, 4.5, 5) # Standard deviation
# Aircraft capacity
total_seats <- 180
n_classes <- length(fare)
# Initialize protection levels
protection_levels <- numeric(n_classes)
# Calculate protection for each fare class (except lowest)
for (j in 1:(n_classes - 1)) {
mu_combined <- sum(mu[1:j]) # Cumulative mean demand
sigma_combined <- sqrt(sum(sigma[1:j]^2)) # Cumulative standard deviation
z_ratio <- 1 - fare[j + 1] / fare[j] # Fare ratio for z-score
z_score <- qnorm(z_ratio)
protection_levels[j] <- mu_combined + z_score * sigma_combined
}
protection_levels[n_classes] <- 0 # No protection for lowest fare class
The EMSRb method ensures that high-value customers (paying higher fares) are prioritized, especially during peak booking periods. By protecting seats for classes with higher fares, the airline can increase total revenue compared to a first-come, first-served approach. The z-score reflects the trade-off between accepting a lower fare now versus reserving seats for a potentially higher fare later.
To present the EMSRb results clearly, we create a text-based table embedded in the plot, showing: - Class: Fare class (F1 to F10, with F1 being the highest fare). - Fare: Ticket price in EUR. - Protection: Number of seats reserved for classes 1 to \(j\).
The table uses a monospaced font (mono) and fixed-width
columns for alignment, ensuring readability in the visualization. This
approach avoids external table dependencies, making the output
self-contained.
# Create EMSRb table
emsr_table <- data.frame(
Class = paste0("F", 1:n_classes),
Fare = fare,
Protection = round(protection_levels, 1)
)
# Format table as text with aligned columns
table_text <- c(
"Class | Fare | Protection",
"------|------|-----------",
sprintf("%-5s | %4d | %9.1f", emsr_table$Class, emsr_table$Fare, emsr_table$Protection)
)
table_text <- paste(table_text, collapse = "\n")
The table is placed in the upper right of the plot, using purple text to distinguish it from the revenue curve and optimal price annotations. This placement maximizes visibility without obscuring the revenue curve’s peak.
The visualization integrates: - Revenue Curve: Shows expected revenue versus price, peaking at the optimal price. - Optimal Price: Marked with a dashed vertical line and annotated with \(p^*\). - EMSRb Table: Displays seat protection levels in the upper right. - Labels: Include LaTeX-formatted equations for mathematical precision, styled with markdown for clarity.
The plot uses a minimal theme for a clean, professional appearance, tailored for the KEF-LHR route. The subtitle highlights the exponential demand model and EMSRb methodology, providing context for the viewer.
plot <- ggplot(df, aes(x = Price)) +
geom_line(aes(y = ExpectedRevenueTotal), color = "steelblue", size = 1.2) +
# Optimal price marker
geom_vline(xintercept = p_star, linetype = "dashed", color = "darkred", size = 1) +
annotate("text", x = p_star + 5, y = max(df$ExpectedRevenueTotal)*0.95,
label = TeX(paste0("Optimal~Price~p^*~=", round(p_star, 2), "~EUR")),
color = "darkred", hjust = 0) +
# EMSRb Protection Table (upper right)
annotate("text", x = p_max - 20, y = max(df$ExpectedRevenueTotal)*0.95,
label = table_text, color = "purple", size = 3.5, hjust = 0.5, vjust = 1,
family = "mono") +
labs(
title = "Exponential Pricing Curve + EMSRb Seat Protection",
subtitle = "<span style='color:darkred;'>Model: Pr(Pt ≥ p) = exp(-αp)</span> | <span style='color:purple;'>EMSRb: yj = mu_1j + zj * sigma_1j</span>",
x = "Price (EUR)",
y = TeX("Expected Revenue: $Pr(P_t \\geq p) \\cdot (p - \\Delta v)$")
) +
scale_y_continuous(labels = comma) +
theme_minimal() +
theme(
plot.title = element_text(size = 15, face = "bold"),
plot.subtitle = element_markdown(size = 10),
axis.title = element_text(size = 13)
)
print(plot)
ggsave("exponential_pricing_plot.png", plot = plot, width = 8, height = 6, dpi = 300)
This notebook demonstrates an approach to airline revenue management, combining an exponential demand model with EMSRb seat protection for the route. The exponential model captures the high price sensitivity of leisure travelers, while EMSRb ensures optimal seat allocation for higher-paying customers. The visualization provides an output for pricing and capacity decisions.
For further details, please refer to DOI:10.1016/j.jmaa.2019.06.012, Talluri & van Ryzin (2004), and Mumbower et al. (2014). The methodology is grounded in established revenue management literature, making it a valuable addition to a data science portfolio.
Generated with R Markdown. Requires R packages: ggplot2, latex2exp, scales, ggtext.