Horizontal merger simulations

Introduction

This repository contains functions that can be used to replicate the results from Panhans and Taragin (2023) “Consequences of model choice in predicting horizontal merger effects” https://doi.org/10.1016/j.ijindorg.2023.102986. This vignette illustrates how the functions can be used for the calibration and merger simulation of all four models analyzed in the paper.

Simple models of competition that can be calibrated with only a few inputs can be useful to readily analyze certain mergers. For example, such an exercise can be useful when screening for possible concern in a transaction. Even with simple models, however, there are several nuances to consider. This document covers four models of differentiated product competition analyzed in Panhans and Taragin 2023: Bertrand, second score auction, a classic Nash bargaining model that nests Bertrand, and an alternative bargaining model formulation that nests the second score auction.

Preliminaries

First, we load the mergersim package and others that will be useful. BB and rootSolve are packages with optimization tools. The package numDeriv is needed for the jacobian() function.

library(mergersim)
library(BB)
library(rootSolve)
library(numDeriv)

In this document, we will assume a market with three suppliers and many customers. Customer \(i\) has utility for good \(j\): \[\begin{equation*} u_{ij} = \delta_j + \alpha p_j + \varepsilon_{ij} \end{equation*}\]

We will assume that the preference shock follows the logit or type 1 extreme value distribution.

Suppose that the true marginal cost of production for each good \(j\) is known, and let those costs and the true demand parameters be given by:

alpha_true  <- -0.9
delta_true <- c(.81,.93,.82)
c_j <- c(.05,.31,.30)

Suppose that pre-merger, all three goods are owned by independent suppliers, and that we are investigating a transaction between firms 1 and 2. Then, we can create ownership matrices to reflect the pre- and post-merger market, which we will use later:

own_pre = diag(3)
own_post <- own_pre
own_post[1,2] <- 1
own_post[2,1] <- 1

Bertrand Competition

The Bertrand model of competition posits that firms offer differentiated products and compete on the basis of price setting. In particular, for a firm \(n\) that owns product portfolio \(W_n\), specify the profit function as:

\[\begin{equation*} \Pi^n = \sum_{k \in W_n} (p_k - c_k) \cdot s_k(p) \end{equation*}\]

Taking the derivative of each firm’s profit function with respect to each owned good’s price yields a set of \(J\) first order conditions that can be expressed, for each good \(j \in J\) as: \[\begin{equation*} \sum_{k \in W_n} (p_k - c_k) \cdot \frac{\partial s_k}{\partial p_j} + s_j = 0 \end{equation*}\] where \(W_n\) denotes the set of goods owned by the same firm that owns good \(j\).

This system of FOC’s can be expressed in matrix notation. Let \(\Omega\) denote the J-by-J ownership matrix, \(t(dd)\) denotes the transpose of a matrix where element \((j,k)\) equals \(\frac{\partial s_j}{\partial p_k}\), \(m\) is vector length \(J\) of margins where the jth element is \((p_j - c_j)\), and \(s\) is a vector of length \(J\) of product market shares. Then the stacked FOC’s can be denoted as:

\[\begin{equation*} 0 = (\Omega * t(dd)) \%*\% m + s \end{equation*}\]

We can use the true demand parameters and marginal costs to determine the equilibrium prices and shares. First, define the Bertrand FOC function:

Then use multiroot to find the equilibrium prices that set the FOCs to zero:

x0 <- c_j*1.1
out1 <- multiroot(f = bertrand_foc, start = x0, 
                  own = own_pre, alpha= alpha_true, 
                  delta = delta_true, cost = c_j)

p1 <- out1$root
p1
#> [1] 1.482363 1.709577 1.673102
share1 <- (exp(delta_true + alpha_true*p1))/(1+sum(exp(delta_true + alpha_true*p1)))
share1
#> [1] 0.2242812 0.2061096 0.1908020

The shares do not sum to one because there is also an outside option.

To find the new equilibrium after the merger, simply pass the post-merger market structure matrix to the function.

x0 <- c_j*1.1
out1_post <- multiroot(f = bertrand_foc ,start = x0, 
                  own = own_post, alpha= alpha_true, 
                  delta = delta_true, cost = c_j)

p1_post <- out1_post$root
p1_post
#> [1] 1.793086 2.053067 1.705423
share1_post <- (exp(delta_true + alpha_true*p1_post))/(1+sum(exp(delta_true + alpha_true*p1_post)))
share1_post
#> [1] 0.1916001 0.1709596 0.2094127

# margins. Note that multi-product firm sets same level margins.
(p1_post - c_j)
#> [1] 1.743086 1.743067 1.405423
(p1_post - c_j)/p1_post
#> [1] 0.9721151 0.8490064 0.8240906

# price effect
(p1_post - p1)/p1_post
#> [1] 0.17328924 0.16730575 0.01895219

Calibrating the demand parameters

Suppose now that the equilibrium pre-merger prices and shares are observed, and at least some firms costs are also known, but that the demand parameters need to be obtained in order to conduct a merger simulation. In some cases, the appropriate data and exogenous variation exists such that the demand parameters can be estimated. But other times, it may be difficult to do so credibly, or there may be insufficient time. In such cases, it can be informative to recover demand parameters that are implied by an assumed model of competition for the observed prices and shares to be an equilibrium.

For the Bertrand model, calibration can be done based on the first-order conditions and share equations. There are \(J\) pricing equations and \(J\) share equations, for a total of \(2*J\) equations. In terms of unknown parameters, we have \(J\) unknown \(\delta_j\)’s and one price coefficient \(\alpha\), for a total of \(J+1\) unknown parameters. This means that the model is over-identified for calibration purposes.

Because the model is over-identified, we use a weighting matrix to assign relative weights to the pricing equations and the share equations. Define an objective function to calibrate the Bertrand model, similar to the function defined above, except that now the demand parameters are the parameters to optimize over, and the prices and shares are inputs to be passed to the function.

Then use this function to recover the demand parameters.

x00 <- c(-1)
wt_matrix <- diag(c(1,1,1,1000,1000,1000))

out1 <- optim(f = bertrand_calibrate, par = x00, 
                   own = own_pre, price = p1, 
                   shares = share1, cost  = c_j,
                   weight = wt_matrix)

# note that this optimization recovers the true demand parameters
out1$par
#> [1] -0.9
alpha_true
#> [1] -0.9

delta_cal <- log(share1) - log(1-sum(share1)) - out1$par*p1
delta_cal
#> [1] 0.81 0.93 0.82
delta_true
#> [1] 0.81 0.93 0.82

With the true demand parameters, the function bertrand_foc can be used to generate post-merger equilibrium prices and shares by changing the ownership matrix, and if desired the costs can also be changed to reflect merger efficiencies.

Calibration with some unobserved costs

Typically, costs might be available for only a subset of the firms included in the analysis. For example, in a merger investigation, marginal cost data might only be available from the merging firms. Suppose now that we observe the costs only for firms 1 and 2:

c_obs <- c_j
c_obs[3] <- NA
c_obs
#> [1] 0.05 0.31   NA
x00 <- c(-1)
wt_matrix <- diag(c(1,1,1,1000,1000,1000))

out1b <- optim(f = bertrand_calibrate, par = x00, 
                   own = own_pre, price = p1, 
                   shares = share1, cost  = c_obs,
                   weight = wt_matrix)

# note that this optimization recovers the true demand parameters
out1b$par
#> [1] -0.9
alpha_true
#> [1] -0.9

delta_cal <- log(share1) - log(1-sum(share1)) - out1b$par*p1
delta_cal
#> [1] 0.81 0.93 0.82
delta_true
#> [1] 0.81 0.93 0.82

The calibrated parameters closely match the true parameters. Again, predicted merger effects can be obtained using the bertrand_foc function and changing the ownership matrix, and if desired, the firm costs.

Second score auction

The second score auction model is discussed in detail in Miller (2014) https://doi.org/10.1016/j.ijindorg.2014.10.001. Briefly, it assumes that firms are submitting bids to the customer, and has an equilibrium condition where suppliers’ dominant strategy is to bid their costs. The customer selects the supplier that creates the greatest surplus, and the price is set to make the customer indifferent between the best option at the equilibrium price and obtaining the second best option at cost.

Assuming logit demand, the pricing condition in the second score auction is given by: \[\begin{equation*} p_j = c_j + \frac{1}{\alpha*\sum_{k \in W_j} s_k }\log(1 - \sum_{k \in W_j} s_k) \end{equation*}\] where \(W_j\) denotes the set of all products that are owned by the same firm that owns product \(j\), including product \(j\).

Since we know the true parameters, we can use the margin condition from the SSA model to calculate the equilibrium shares, as well as pre-merger and post-merger prices. Therefore, we can also calculate the true price effect of the merger.

share2 <- (exp(delta_true + alpha_true*c_j))/(1+sum(exp(delta_true + alpha_true*c_j)))

p2 <- c_j + log(1 - own_pre%*%share2)/(alpha_true*own_pre%*%share2)
p2_post <- c_j + log(1 - own_post%*%share2)/(alpha_true*own_post%*%share2)

(p2_post - p2)/p2
#>           [,1]
#> [1,] 0.2582731
#> [2,] 0.2402088
#> [3,] 0.0000000

Calibrating the demand parameters

Suppose now that the equilibrium pre-merger prices and shares are observed, and at least some firms costs are also known, but that the demand parameters need to be obtained in order to conduct a merger simulation.

For the second score auction, calibration can be done based on the second score pricing conditions and share equations. There are \(J\) pricing equations and \(J\) share equations, for a total of \(2*J\) equations. In terms of unknown parameters, we have \(J\) unknown \(\delta_j\)’s and one price coefficient \(\alpha\), for a total of \(J+1\) unknown parameters. This means that the model is over-identified for calibration purposes.

Because the model is over-identified, we use a weighting matrix to assign relative weights to the pricing equations and the share equations. The second score auction calibration function searches for demand parameters that best match model predicted shares and prices to the observed shares and prices. Specifically, the function below minimizes the squared sum of the difference between predicted and observed values, and uses the weighting matrix to assign relative weights to each equation.

Rather than jointly optimizing over all of the demand parameters, it is computationally faster to first calibrate only the price coefficient \(\alpha\) using the available pricing equations, and then recover the \(\delta\) parameters from the share equations once the price coefficient is known.

Only one good’s marginal cost needs to be known to be able to calibrate the model, and often times cost information will only be readily available from merging firms. We can use ssa_calibrate to find \(\alpha\) using only goods that have cost information available.

Suppose that we observe only the costs of firms 1 and 2:

c_obs <- c_j
c_obs[3] <- NA
c_obs
#> [1] 0.05 0.31   NA

Now we calibrate \(\alpha\) with the available pricing conditions:

wt_matrix <- diag(c(1,1))

result4 <- BBoptim(f = ssa_calibrate, par = c(-.2),
                    lower = c(-Inf), upper = c(-0.0001),
                    own=own_pre, price = p2, share = share2,
                    cost = c_obs, weight = wt_matrix)
#> iter:  0  f-value:  42.71919  pgrad:  549.2472 
#> iter:  10  f-value:  0.0003334565  pgrad:  0.07430411 
#>   Successful convergence.

alpha4 <- result4$par
alpha4       # recover true value
#> [1] -0.9000002

With \(\alpha\) recovered, we can now recover any missing costs:

cost4 <- p2 - log(1 - own_pre%*%share2) / (alpha4*own_pre%*%share2)
cost4
#>            [,1]
#> [1,] 0.05000023
#> [2,] 0.31000022
#> [3,] 0.30000022

The recovered costs match the true costs:

c_j
#> [1] 0.05 0.31 0.30

This is sufficient information to predict the effects of a merger between firms 1 and 2. The predicted effects using the calibrated parameters match the true unilateral merger effects.

p4 <- cost4 + log(1 - own_pre%*%share2)/(alpha4*own_pre%*%share2)
p4_post <- cost4 + log(1 - own_post%*%share2)/(alpha4*own_post%*%share2)

(p4_post - p4)/p4
#>           [,1]
#> [1,] 0.2582731
#> [2,] 0.2402088
#> [3,] 0.0000000

Note that if the cost is observed for only one product, the model is exactly identified, and the true price coefficient parameter \(\alpha\) can be calculated analytically. For example, the calculation below recovers the true price coefficient assuming that only the cost/margin of product 1 is available. The optimizer used above is useful when margins are available for more than one product.

1/(p4[1] - c_obs[1]) * log(1 - share2[1]) / share2[1]
#> [1] -0.9

Traditional Bargaining Model (nests Bertrand)

For now, we skip the mathematical framework for the bargaining models, but the equations are given in many papers including Panhans and Taragin (2023). Instead, in this document we focus on defining functions that will allow us to generate equilibrium prices and shares, and then recover the true demand parameters from those equilibrium objects. We first consider a traditional bargaining setup that nests the Bertrand model.

Let’s assume the demand parameters specified above are the true demand parameters, and generate equilibrium market characteristics.

x0 <- c_j*1.5

out3 <- multiroot(f = bargain_foc, start = x0, own = own_pre, 
                  alpha = alpha_true, delta = delta_true, cost = c_j,
                  lambda = 0.5)
p3 <- out3$root
share3 <- (exp(delta_true + alpha_true*p3))/(1+sum(exp(delta_true + alpha_true*p3)))

print(p3)
#> [1] 0.7543970 0.9968632 0.9731435
print(share3)
#> [1] 0.2767645 0.2508731 0.2295900

Calibration

The calibration function can be used to recover the demand parameters from these prices, shares, and costs.

J <- length(c_j)
alpha_start <- -1.2
delta_start <- rep(1,J)
x00 <- c(alpha_start,delta_start)
wt_matrix <- diag(J*2)


bargain_calibrate(param = x00, 
                       own = own_pre, price = p3, 
                       shares = share3, cost  = c_j,
                       weight = wt_matrix, lambda = 0.5)
#>            [,1]
#> [1,] 0.08717853


out3 <- BBoptim(f = bargain_calibrate, par = x00, 
                own = own_pre, price = p3, 
                shares = share3, cost  = c_j,
                weight = wt_matrix, lambda = 0.5)
#> iter:  0  f-value:  0.08717853  pgrad:  0.4463191 
#> iter:  10  f-value:  0.0001924759  pgrad:  0.00761619 
#> iter:  20  f-value:  7.093329e-05  pgrad:  0.0005777945 
#> iter:  30  f-value:  2.524549e-05  pgrad:  0.002185755 
#> iter:  40  f-value:  3.140482e-08  pgrad:  3.498634e-05 
#>   Successful convergence.

# check if we recovered correct demand parameters
# finding good initial values is important.
alpha3 <- out3$par[1]
delta3 <- out3$par[2:4]
alpha_true
#> [1] -0.9
alpha3
#> [1] -0.9000906
delta_true
#> [1] 0.81 0.93 0.82
delta3
#> [1] 0.8116445 0.9317579 0.8218446

Alternative Bargaining Model (nests SSA)

Next, we consider an alternative bargaining setup that nests the second score auction.

Let’s assume the demand parameters specified above are the true demand parameters, and generate equilibrium market characteristics.

x0 <- c_j*1.5

out4 <- multiroot(f = ssbargain_foc, start = x0, own = own_pre, 
                       alpha = alpha_true, delta = delta_true, 
                  cost = c_j, lambda = 0.5)
p4 <- out4$root
share4 <- (exp(delta_true + alpha_true*c_j))/(1+sum(exp(delta_true + alpha_true*c_j)))

print(p4)
#> [1] 0.7177362 0.9626474 0.9412909
print(share4)
#> [1] 0.3160423 0.2819913 0.2549012

Then we can use the calibration function to recover the demand parameters from these prices, shares, and costs.

alpha_start <- -1.2
delta_start <- rep(1,J)
x00 <- c(alpha_start,delta_start)
wt_matrix <- diag(J*2)

out4 <- BBoptim(f = ssbargain_calibrate, par = x00, 
                own = own_pre, price = p4, 
                shares = share4, cost  = c_j,
                weight = wt_matrix, lambda = 0.5)
#> iter:  0  f-value:  0.0806167  pgrad:  0.4043435 
#> iter:  10  f-value:  9.529025e-05  pgrad:  0.001538167 
#> iter:  20  f-value:  1.905773e-05  pgrad:  0.0003214364 
#> iter:  30  f-value:  2.18451e-06  pgrad:  0.0001603663 
#> iter:  40  f-value:  1.439968e-06  pgrad:  0.0006213753 
#>   Successful convergence.

# check if we recovered correct demand parameters
# finding good initial values is important.
alpha4 <- out4$par[1]
delta4 <- out4$par[2:4]
alpha_true
#> [1] -0.9
alpha4
#> [1] -0.9000104
delta_true
#> [1] 0.81 0.93 0.82
delta4
#> [1] 0.8103971 0.9304098 0.8204080

Generalized Nested Logit (GNL) Demand

All of the models above have been implemented with the standard logit demand assumption. The same functions can be used to implement a Generalized Nested Logit demand system, which allows for overlapping nests and includes nested logit and standard logit as special cases.

Here, I’ll illustrate the implementation of GNL with the Bertrand model.

Bertrand

First, we define the GNL structure. \(K\) indicates the number of nests. \(B\) indicates the nests each good belongs to: row \(j\) column \(k\) is 1 if good \(j\) belongs to nest \(k\). \(a\) is a matrix of the same dimension as \(B\) that designates each good’s degree of membership in each nest. For ease of interpretation, the rows of \(a\) should sum to 1 (meaning each good should have a member degree in each of its nests that sum to 1). \(\mu\) is a vector of length \(K\) that represents the nesting parameter for each nest.

K1 <- 2

B1 <- 1 * matrix( c(1,0,
               1,0,
               0,1),
             ncol = K1, nrow = J, byrow = TRUE)
a1 <- B1    # rows of a should sum to 1 to facilitate interpretation.
mu1 <- rep(1.0,K1) # nesting parameters all 1 simplifies to logit
mu2 <- c(0.8,0.8)

We will skip calibration and assume we have the true demand parameters, and just need to simulate the equilibrium.

First, let’s use the GNL function but input values for logit demand and show that we get the same answer that we got above.


x0 <- p1*1.1

out1 <- BBoptim(fn = bertrand_foc, par = x0, 
                own = own_pre, alpha = alpha_true, 
                delta = delta_true, cost = c_j, sumFOC = TRUE)

p_R1 <- out1$par
# Equilibrium prices are the same as we had from logit model before
p_R1
p1
  

But when we provide the function with the GNL structure that is different than the standard logit, we get a different result.


x0 <- p1*1.1

out2 <- BBoptim(fn = bertrand_foc, par = x0, 
                own = own_pre, alpha = alpha_true, 
                delta = delta_true, cost = c_j,
                nest_allocation=a1, mu=mu2,
                sumFOC = TRUE)

p_R2 <- out2$par
# Equilibrium prices are different than logit result
p_R2
#> [1] 1.298512 1.515000 1.669673

It may sometimes also be useful to have a function that computes a diversion ratio matrix. Note that for nested logit and generalized nested logit, diversion ratios computed based on a marginal price increase are different than those computed from removing a choice from the choice set. See Conlon and Mortimer (2021) ``Empirical Properties of Diversion Ratios’’ for more details.

This function can be used to obtain a diversion ratio matrix.

diversions <- diversion_calc(price=p_R2,alpha=alpha_true,delta=delta_true,
                             nest_allocation=a1,mu=mu2)

diversions
#>           [,1]      [,2]      [,3]
#> [1,] 0.0000000 0.4027067 0.2004861
#> [2,] 0.4206808 0.0000000 0.1944529
#> [3,] 0.2824859 0.2572581 0.0000000
rowSums(diversions) # each row should sum to <1 because of outside option
#> [1] 0.6031927 0.6151337 0.5397440

The above diversion ratio matrix is based on average diversion rates, which are computed based on removing a product from the choice set. Alternatively, one may be interested in marginal diversion ratios, which are the diversion rates based on a marginal increase in price for a good. For a standard logit demand system, these two diversion ratios are identical. But for more general demand systems, these two diversion ratios will not be identical. The marginal diversion can be computed by using the ‘marginal’ option in the function.

diversions2 <- diversion_calc(price=p_R2,alpha=alpha_true, delta=delta_true,
                              nest_allocation=a1, mu=mu2, marginal = TRUE)

diversions2
#>           [,1]      [,2]      [,3]
#> [1,] 0.0000000 0.3687388 0.2118876
#> [2,] 0.3907889 0.0000000 0.2044863
#> [3,] 0.2824859 0.2572581 0.0000000
rowSums(diversions2) # each row should sum to <1 because of outside option
#> [1] 0.5806264 0.5952753 0.5397440