This notebook implements three pricing models for airline tickets on the KEF-LHR route, using data sourced from Skyscanner (21 April 2025): the Exponential, Isoelastic, and Logarithmic demand models. The methodology draws from Table 1 and Section 3.1 of the research paper DOI:10.1016/j.jmaa.2019.06.012, Talluri & van Ryzin (2004), and Mumbower et al. (2014).
Each model achieves: - Optimal Price Identification: Finds the revenue-maximizing price, sensitive to market dynamics. - EMSRb Seat Protection: Allocates seats to higher fare classes using the Expected Marginal Seat Revenue (EMSRb) method.
Visualizations combine revenue curves, optimal prices, and EMSRb protection tables, providing comprehensive tools for airline revenue management.
# Load libraries for plotting, LaTeX rendering, and numerical optimization
library(ggplot2) # For creating visualizations
library(latex2exp) # For rendering LaTeX equations in plots
library(scales) # For formatting plot scales
library(ggtext) # For markdown in plot labels
library(stats) # For numerical optimization in logarithmic model
The model uses real-world pricing data from Skyscanner for the KEF-LHR route, collected for 21 April 2025. The dataset includes 10 fare classes. Conversion rates (probabilities of booking at each price) are assumed 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}\). This equation models the exponential decline in booking probability with price increases.
The marginal value, \(\Delta v\), accounts for operational costs (e.g., fuel, crew) and no-show adjustments: \(\Delta v = \frac{\min(\text{prices}) + \max(\text{prices})}{3}\). This formula, derived from the research paper, approximates the per-seat cost baseline.
# Define observed prices from Skyscanner data for KEF-LHR route on 21 April 2025
prices <- c(241, 246, 247, 252, 255, 258, 261, 270, 271, 294)
# Assumed conversion rates based on market behavior for leisure travelers
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_rate) = intercept - alpha * prices
log_conversion <- log(conversion_rate)
model_exp <- lm(log_conversion ~ prices)
alpha <- -coef(model_exp)[2] # alpha is the negative of the slope coefficient
# Calculate marginal value delta_v as per the research paper's methodology
delta_v <- (min(prices) + max(prices)) / 3
The parameter \(\alpha\) quantifies demand sensitivity; a higher \(\alpha\) indicates a steeper booking decline, typical for leisure routes. The \(\Delta v\) value ensures net profit calculations. Conditions include \(\alpha > 0\) to ensure decreasing demand and prices within 241–294 EUR.
The exponential demand model, as detailed in DOI:10.1016/j.jmaa.2019.06.012, assumes: \(\Pr(P_t \geq p) = e^{-\alpha p}\). This probability represents the likelihood that a customer’s reservation price is at least \(p\), ideal for markets with rapid demand drops.
The expected revenue per seat is: \(\text{Expected Revenue} = \Pr(P_t \geq p) \cdot (p - \Delta v)\), balancing booking probability and profit margin.
The optimal price is: \(p^* = \Delta v + \frac{1}{\alpha}\), derived by maximizing the revenue function, with \(\alpha > 0\) ensuring a finite price.
The revenue curve is computed over 190–320 EUR with 500 points to visualize revenue behavior.
# Set price range for plotting revenue curve
p_min <- 190
p_max <- 320
p_points <- 500
p <- seq(p_min, p_max, length.out = p_points)
# Calculate Pr(P_t >= p) for exponential model: exp(-alpha * p)
pr_ge_p_exp <- exp(-alpha * p)
# Calculate expected revenue: Pr(P_t >= p) * (p - delta_v)
expected_revenue_exp <- pr_ge_p_exp * (p - delta_v)
# Calculate optimal price: delta_v + 1/alpha
p_star_exp <- delta_v + 1 / alpha
# Create data frame for plotting
df_exp <- data.frame(Price = p, ExpectedRevenueTotal = expected_revenue_exp)
The optimal price \(p^*\) decreases with higher \(\alpha\), aligning with price-sensitive markets.
The Expected Marginal Seat Revenue (EMSRb) method, introduced by Belobaba (1989) and detailed in Talluri & van Ryzin (2004), optimizes seat allocation for an Airbus A320 (180 seats) by reserving seats for higher fare classes: \(y_j = \mu_{1:j} + z_j \cdot \sigma_{1:j}\), where \(\mu_{1:j} = \sum_{i=1}^j \mu_i\), \(\sigma_{1:j} = \sqrt{\sum_{i=1}^j \sigma_i^2}\), and \(z_j = \Phi^{-1}(1 - \frac{f_{j+1}}{f_j})\). This formula calculates the number of seats to protect for higher fare classes based on demand variability and fare ratios.
# Sort fares in descending order for EMSRb calculation
fare <- sort(prices, decreasing = TRUE)
# Define assumed mean demand and standard deviation for each fare class
mu <- c(5, 7, 9, 10, 12, 14, 16, 18, 20, 22)
sigma <- c(2, 2.5, 2.5, 3, 3, 3.5, 3.5, 4, 4.5, 5)
# Set aircraft capacity
total_seats <- 180
n_classes <- length(fare)
# Initialize protection levels vector
protection_levels <- numeric(n_classes)
# Calculate protection levels for each fare class except the lowest
for (j in 1:(n_classes - 1)) {
# Cumulative mean demand for classes 1 to j
mu_combined <- sum(mu[1:j])
# Cumulative standard deviation for classes 1 to j
sigma_combined <- sqrt(sum(sigma[1:j]^2))
# Fare ratio for z-score calculation
z_ratio <- 1 - fare[j + 1] / fare[j]
# Z-score from inverse normal CDF
z_score <- qnorm(z_ratio)
# Protection level: mean plus z-score times standard deviation
protection_levels[j] <- mu_combined + z_score * sigma_combined
}
# Set no protection for the lowest fare class
protection_levels[n_classes] <- 0
EMSRb prioritizes high-value customers, increasing revenue over first-come, first-served approaches.
The table shows fare class, fare, and protection level, using a monospaced font for alignment.
# Create EMSRb table with class, fare, and protection levels
emsr_table <- data.frame(
Class = paste0("F", 1:n_classes),
Fare = fare,
Protection = round(protection_levels, 1)
)
# Format table as text with aligned columns for plot annotation
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 each plot, in purple for distinction.
The visualization includes: - Revenue curve peaking at \(p^*\). - Optimal price with a dashed line. - EMSRb table in the upper right. - LaTeX-formatted labels.
# Create plot for exponential model revenue curve
plot_exp <- ggplot(df_exp, aes(x = Price)) +
geom_line(aes(y = ExpectedRevenueTotal), color = "steelblue", size = 1.2) +
geom_vline(xintercept = p_star_exp, linetype = "dashed", color = "darkred", size = 1) +
annotate("text", x = p_star_exp + 5, y = max(df_exp$ExpectedRevenueTotal)*0.95,
label = TeX(paste0("Optimal~Price~p^*~=", round(p_star_exp, 2), "~EUR")),
color = "darkred", hjust = 0) +
annotate("text", x = p_max - 20, y = max(df_exp$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)
)
# Display and save plot
print(plot_exp)
ggsave("exponential_pricing_plot.png", plot = plot_exp, width = 8, height = 6, dpi = 300)
The model uses the same Skyscanner data and conversion rates.
Parameters \(a\) and \(b\) are estimated using: \(\log(\text{conversion_rate}) = \log(a) - b \cdot \log(\text{prices})\). This regression models the power-law relationship of the isoelastic demand, as per DOI:10.1016/j.jmaa.2019.06.012.
The marginal value is: \(\Delta v = \frac{\min(\text{prices}) + \max(\text{prices})}{3}\).
Conditions: \(a > 0\), \(b > 1\).
# Estimate isoelastic parameters a and b using log-linear regression
log_prices <- log(prices)
model_iso <- lm(log_conversion ~ log_prices)
a <- exp(coef(model_iso)[1]) # Intercept gives log(a)
b <- -coef(model_iso)[2] # Slope gives -b
The parameter \(b\) represents price elasticity; \(b > 1\) ensures demand decreases with price. The parameter \(a\) scales the demand curve. The support condition \(p \geq a^{1/b}\) ensures valid prices.
The isoelastic model, as described in DOI:10.1016/j.jmaa.2019.06.012, assumes: \(\Pr(P_t \geq p) = a p^{-b}\), reflecting constant price elasticity, suitable for stable markets.
The expected revenue is: \(\text{Expected Revenue} = \Pr(P_t \geq p) \cdot (p - \Delta v)\).
The optimal price is: \(p^* = \Delta v \cdot \frac{b}{b-1}\), provided \(p^* \geq a^{1/b}\); otherwise, \(p^* = a^{1/b}\).
The revenue curve enforces the support condition.
# Calculate Pr(P_t >= p) for isoelastic model: a * p^(-b)
pr_ge_p_iso <- a * p^(-b)
pr_ge_p_iso[p < a^(1/b)] <- 0 # Enforce support p >= a^(1/b)
# Calculate expected revenue
expected_revenue_iso <- pr_ge_p_iso * (p - delta_v)
# Calculate optimal price with support condition
p_star_iso <- delta_v * b / (b - 1)
if (p_star_iso < a^(1/b)) p_star_iso <- a^(1/b)
# Create data frame for plotting
df_iso <- data.frame(Price = p, ExpectedRevenueTotal = expected_revenue_iso)
The optimal price depends on \(b\); higher elasticity reduces \(p^*\). The support condition ensures realistic prices.
The visualization mirrors the exponential model.
# Create plot for isoelastic model revenue curve
plot_iso <- ggplot(df_iso, aes(x = Price)) +
geom_line(aes(y = ExpectedRevenueTotal), color = "steelblue", size = 1.2) +
geom_vline(xintercept = p_star_iso, linetype = "dashed", color = "darkred", size = 1) +
annotate("text", x = p_star_iso + 5, y = max(df_iso$ExpectedRevenueTotal, na.rm = TRUE)*0.95,
label = TeX(paste0("Optimal~Price~p^*~=", round(p_star_iso, 2), "~EUR")),
color = "darkred", hjust = 0) +
annotate("text", x = p_max - 20, y = max(df_iso$ExpectedRevenueTotal, na.rm = TRUE)*0.95,
label = table_text, color = "purple", size = 3.5, hjust = 0.5, vjust = 1,
family = "mono") +
labs(
title = "Isoelastic Pricing Curve + EMSRb Seat Protection",
subtitle = "<span style='color:darkred;'>Model: Pr(Pt ≥ p) = a p^{-b}</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)
)
# Display and save plot
print(plot_iso)
ggsave("isoelastic_pricing_plot.png", plot = plot_iso, width = 8, height = 6, dpi = 300)
The model uses the same Skyscanner data and conversion rates.
Price bounds are \(p_{up} = \max(\text{prices})\), \(p_{low} = \min(\text{prices})\), with \(\kappa = \ln(p_{up}/p_{low})\). The marginal value is: \(\Delta v = \frac{\min(\text{prices}) + \max(\text{prices})}{3}\).
Conditions: \(p_{up} > p_{low} > 0\).
# Define price bounds and kappa for logarithmic model
p_up <- max(prices)
p_low <- min(prices)
kappa <- log(p_up / p_low)
The bounds define the reservation price range; \(\kappa\) normalizes the logarithmic decay.
The logarithmic model, as detailed in DOI:10.1016/j.jmaa.2019.06.012, assumes: \(\Pr(P_t \geq p) = \frac{\ln(p_{up}) - \ln(p)}{\ln(p_{up}) - \ln(p_{low})}\), modeling bounded willingness to pay.
The expected revenue is: \(\text{Expected Revenue} = \Pr(P_t \geq p) \cdot (p - \Delta v)\).
The optimal price satisfies: \(p^* = p^* \kappa \Pr(P_t \geq p^*) + \Delta v\), solved numerically within \([\max\{p_{low}, p_{up}/e\}, p_{up}]\).
The optimal price is found numerically.
# Calculate Pr(P_t >= p) for logarithmic model
pr_ge_p_log <- (log(p_up) - log(p)) / kappa
pr_ge_p_log[p < p_low | p > p_up] <- 0 # Enforce support [p_low, p_up]
# Calculate expected revenue
expected_revenue_log <- pr_ge_p_log * (p - delta_v)
# Define objective function to find optimal price numerically
objective <- function(p) {
pr <- (log(p_up) - log(p)) / kappa
(p - (p * kappa * pr + delta_v))^2
}
# Optimize within the valid interval
p_star_log <- optimize(objective, c(max(p_low, p_up/exp(1)), p_up))$minimum
# Create data frame for plotting
df_log <- data.frame(Price = p, ExpectedRevenueTotal = expected_revenue_log)
The logarithmic model captures bounded price sensitivity.
The visualization mirrors the previous models.
# Create plot for logarithmic model revenue curve
plot_log <- ggplot(df_log, aes(x = Price)) +
geom_line(aes(y = ExpectedRevenueTotal), color = "steelblue", size = 1.2) +
geom_vline(xintercept = p_star_log, linetype = "dashed", color = "darkred", size = 1) +
annotate("text", x = p_star_log + 5, y = max(df_log$ExpectedRevenueTotal, na.rm = TRUE)*0.95,
label = TeX(paste0("Optimal~Price~p^*~=", round(p_star_log, 2), "~EUR")),
color = "darkred", hjust = 0) +
annotate("text", x = p_max - 20, y = max(df_log$ExpectedRevenueTotal, na.rm = TRUE)*0.95,
label = table_text, color = "purple", size = 3.5, hjust = 0.5, vjust = 1,
family = "mono") +
labs(
title = "Logarithmic Pricing Curve + EMSRb Seat Protection",
subtitle = "<span style='color:darkred;'>Model: Pr(Pt ≥ p) = [ln(p_{up}) - ln(p)] / [ln(p_{up}) - ln(p_{low})]</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)
)
# Display and save plot
print(plot_log)
ggsave("logarithmic_pricing_plot.png", plot = plot_log, width = 8, height = 6, dpi = 300)
This notebook implements Exponential, Isoelastic, and Logarithmic demand models with EMSRb seat protection for the KEF-LHR route. The Exponential model suits price-sensitive markets, Isoelastic offers stable elasticity, and Logarithmic handles bounded price ranges. Visualizations support pricing and capacity decisions.
For further details, refer to DOI:10.1016/j.jmaa.2019.06.012, Talluri & van Ryzin (2004), and Mumbower et al. (2014).
Generated with R Markdown. Requires R packages: ggplot2, latex2exp, scales, ggtext, stats.