Introduction

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.

Step 1: Load Required Libraries

library(ggplot2)
library(latex2exp)
library(scales)
library(ggtext)

Step 2: Define Data and Estimate Alpha

Data Source and Context

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.

Methodology

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

Interpretation

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.

Step 3: Calculate Exponential Revenue Model

Model Overview

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}\).

Implementation

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
)

Insights

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.

Step 4: Calculate EMSRb Protection Levels

EMSRb Methodology

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).

Assumptions

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

Implications

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.

Step 5: Format the EMSRb Table

Table Design

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")

Visualization Integration

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.

Step 6: Visualize the Pricing Model

Plot Components

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)

Plot Details

  • Revenue Curve: The blue curve visualizes how revenue changes with price according to the model formula, peaking at \(p^*\). This helps airlines identify the price that balances booking probability and profit margin.
  • Optimal Price: The dashed line and annotation highlight \(p^*\), making it easy to interpret the model’s recommendation.
  • EMSRb Table: The table shows how many seats are protected for each fare class, aiding in capacity planning.

Conclusion

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.