Vertical merger simulations

Introduction

A model of a vertical supply chain like in Sheu and Taragin (2021) ``Simulating mergers in a vertical supply chain with bargaining’’ can be used to evaluate effects of vertical mergers. That model uses a simultaneous timing assumption for equilibrium. This document shows how to calibrate the model and implement a merger simulation.

Preliminaries

First, we load packages that will be useful. BB and rootSolve are packages with optimization tools.

library(mergersim)

library(BB)
library(rootSolve)
library(antitrust)
library(numDeriv)  # for jacobian() function

Consider a market where \(W\) denotes the set of wholesalers and \(R\) denotes the set of retailers. Denote a specific wholesaler by \(w\) and a specific retailer by \(r\). It will also be useful to define \(R^w\) as the set of retailers that carry \(w\)’s products, and \(W^r\) is the set of wholesalers that supply retailer \(r\). For simplicity, we will assume in this exercise that before any mergers occur, each wholesaler supplies only one product.

Final consumers are indexed by \(i\) and select one retailer at which to purchase one wholesaler’s product. The utility received by consumer \(i\) for purchasing from retailer \(r\) the product owned by wholesaler \(w\) is given by:

\[\begin{equation*} u_{irw} = \delta_{rw} + \alpha p_{rw} + \varepsilon_{irw} \end{equation*}\]

Where \(\alpha < 0\) is the price sensitivity and we assume that the idiosyncratic error term follows the type 1 extreme value distribution.

In the example, we will assume that there are \(|W|=2\) wholesalers and \(|R|=3\) retailers. Each good \(j\) is a retailer-wholesaler combination.

alpha  <- -0.9
R <- 3
W <- 2
delta <- c(0.2, 0.3, 0.9, 1.0, 0.8, 0.9)
c_R_vec <- matrix(.1, nrow = (R*W), ncol = 1)
c_W_vec <- matrix(.2, nrow = (R*W), ncol = 1)

There is also an outside option which is normalized to have \(\delta_{00} = p_{00} = 0\).

Suppose that before any merger occurs, all retailers and suppliers are independently owned. And suppose we are investigating a potential merger between retailer \(r_1\) and wholesaler \(w_1\). Then, we can create ownership vectors to reflect the pre-merger market.

# Define market structure/ownership
own_down_pre <- paste0("R",rep(c(1,2,3),each=2))
own_up_pre <- paste0("W",rep(c(1,2),3))
own_down_pre
#> [1] "R1" "R1" "R2" "R2" "R3" "R3"
own_up_pre
#> [1] "W1" "W2" "W1" "W2" "W1" "W2"

Theoretical Framework

Downstream Bertrand Competition

The Bertrand model of competition posits that downstream retailers offer differentiated products and compete on the basis of price setting. In particular, for a retailer \(r\) that offers product portfolio \(W^r\), specify the profit function as:

\[\begin{equation*} \Pi^r = \sum_{w \in W^r} (p_{rw} - p^W_{rw} - c^R_{rw}) \cdot s_{rw} \cdot M \end{equation*}\]

Which yields a first order condition:

\[\begin{equation} \sum_{x \in W^r} (p_{rx} - p^W_{rx} - c^R_{rx}) \cdot \frac{\partial s_{rx}}{\partial p_{rw}} + s_{rw} = 0 \end{equation}\]

This system of FOC’s can be expressed in matrix notation. Each retailer-wholesaler pair can be denoted as a distinct product, \(j = (r,w)\), and \(J\) denotes the total number of products. Let \(\Omega^R\) denote the J-by-J ownership matrix of downstream products, \(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 - p^W_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^R * 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:

p_W_vec <- matrix(.25, nrow = (R*W), ncol = 1)  # dummy wholesale prices

p_r_start <- c_R_vec*1.1
out1 <- multiroot(f = mergersim:::bertrand_foc_novert ,start = p_r_start, 
                  own_down = own_down_pre, alpha= alpha, 
                  delta = delta, cost = c_R_vec,
                  price_w = p_W_vec)

p1 <- out1$root
p1
#> [1] 1.683099 1.683099 1.889802 1.889802 1.852530 1.852530

As an alternative to multiroot, you could also use BBoptim to find the prices that satisfy the first-order conditions. With data consistent with the model, both methods should yield the same equiblrium prices. It is possible that with some observed data, BBoptim provides a more robust search for equilibrium values.


p_r_start <- as.numeric(c_R_vec*1.1)
out1b <- BBoptim(f = mergersim:::bertrand_foc_novert, par = p_r_start, 
                  own_down = own_down_pre, alpha= alpha, 
                  delta = delta, cost = c_R_vec,
                  price_w = p_W_vec, sumFOC = TRUE)
#> iter:  0  f-value:  0.1973327  pgrad:  0.07226812 
#> iter:  10  f-value:  2.314745e-07  pgrad:  5.893354e-05 
#>   Successful convergence.

p1b <- out1b$par
p1b
#> [1] 1.682608 1.683009 1.889792 1.889778 1.852468 1.852441

Upstream Bargaining

The upstream involves bilateral Nash-in-Nash bargaining between each retailer and wholesaler. Wholesaler payoffs are given as:

\[\begin{equation*} \Pi^w = \sum_{r \in R^w} ( p^W_{rw} - c^W_{rw}) \cdot s_{rw} \cdot M \end{equation*}\]

For each retailer-wholesaler pair \((r,w)\), the Nash bargaining objective function is then given by: \[\begin{equation*} \max_{p^W_{rw}} \left( \Pi^r - d^r(W^r/\{w\}) \right)^\lambda \cdot \left( \Pi^w - d^w(R^w/\{r\}) \right)^{1-\lambda} \end{equation*}\]

where \(d^r\) and \(d^w\) denote the disagreement payoff for retailer \(r\) and wholesaler \(w\), respectively. Equilibrium wholesale prices \(p^W\) are those that maximize this objective function. The first order condition can be used to find the equilibrium prices:

\[\begin{equation} \left( p^W_{rw} - c^W_{rw} \right) \cdot s_{rw} - \sum_{t \in R^w / \{r\}} ( p^W_{tw} - c^W_{tw} ) \cdot \Delta s_{tw} = \frac{1-\lambda}{\lambda} \left( (p_{rw} - p^W_{rw} - c^R_{rw}) \cdot s_{rw} - \sum_{x \in W^r / \{w\}} (p_{rx} - p^W_{rx} - c^R_{rx}) \cdot \Delta s_{rx} \right) \end{equation}\]

where \(\Delta s_{tw}\) is defined as the increase in the share of \(s_{tw}\) that occurs as a result of disagreement between \((r,w)\).

We write a function to define the bargaining first order conditions.

And then with placeholder retail prices, we can then use this first order condition to find equilibrium wholesale prices.

lambda <-  0.5

x0 <- as.numeric(c_W_vec*1.0)

mergersim:::bargain_foc_novert_sim(price_w = x0, own_down = own_down_pre, own_up = own_up_pre, 
         alpha= alpha, delta = delta, 
         cost_w = c_W_vec, cost_r = c_R_vec, 
         lambda = lambda, price_r = (p1*2))
#> [1] 0.04298411

out2 <- BBoptim(par = x0, fn = mergersim:::bargain_foc_novert_sim, 
                own_down = own_down_pre, own_up = own_up_pre,
                alpha= alpha, delta = delta, 
                cost_w = c_W_vec, cost_r = c_R_vec, 
                lambda = lambda, price_r = (p1*1))
#> iter:  0  f-value:  0.03762495  pgrad:  0.02387511 
#> iter:  10  f-value:  9.23201e-08  pgrad:  4.147732e-05 
#>   Successful convergence.

out2$par
#> [1] 0.9778942 0.9954343 1.0257930 1.0433438 1.0177135 1.0352643

p_W2 <- out2$par
shares2 <- (exp(delta + alpha*p1))/(1+sum(exp(delta + alpha*p1)))

In equilibrium, retail prices and wholesale prices need to be jointly determined. The functions defined above can be solved simultaneously to find equilibrium retail and wholesale prices of the model.

# Check if values make sense
print(p_R1)
#> [1] 2.942863 2.974344 3.066814 3.095804 3.044848 3.074182
print(p_W1)
#> [1] 1.609646 1.640788 1.612805 1.641989 1.613095 1.642563

shares1 <- (exp(delta + alpha*p_R1))/(1+sum(exp(delta + alpha*p_R1)))
as.numeric(shares1)
#> [1] 0.04798858 0.05155402 0.08643620 0.09306666 0.07977231 0.08586495
sum(shares1)
#> [1] 0.4446827

# GFT
mergersim:::bargain_foc_novert_sim(price_w = p_W1, own_down = own_down_pre, own_up = own_up_pre, 
         alpha= alpha, 
         delta = delta, cost_w = c_W_vec, cost_r = c_R_vec, lambda = lambda, 
         price_r = p_R1,
         returnGFT = TRUE)
#> $r_gft
#>          [,1]
#> R1 0.05597466
#> R1 0.06037794
#> R2 0.10511455
#> R2 0.11398520
#> R3 0.09632519
#> R3 0.10436054
#> 
#> $w_gft
#>          [,1]
#> W1 0.05580904
#> W2 0.06025085
#> W1 0.10505161
#> W2 0.11386821
#> W1 0.09627565
#> W2 0.10428306

# wholesaler profits
#as.numeric(own_W_pre %*% ((p_W1 - c_W_vec)*shares1) )

# retailer profits
#as.numeric(own_R_pre %*% ((p_R1 - p_W1 - c_R_vec)*shares1) )

FOCs with Vertical Integration

The first order conditions need to be adjusted when there is vertical integration. Let’s adjust the FOCs to account for vertical integration between retailer 1 and wholesaler 1.

And then use these to determine equilibrium prices in a post-merger market.


# Create post ownership
own_up_post <- own_up_pre
own_down_post <- own_down_pre
own_down_post[own_down_post == "R1"] <- "W1"

bertrand_foc_vert(price_r = p_R1,own_down=own_down_post,own_up=own_up_post,
          alpha=alpha,delta=delta,
          cost_r = c_R_vec, price_w = p_W1, cost_w = c_W_vec)
#>             [,1]
#> W1 -4.778884e-02
#> W1  1.405045e-02
#> R2  1.024178e-05
#> R2  2.737772e-05
#> R3 -4.687353e-06
#> R3  5.286390e-06


bargain_foc_vert_sim(price_w = p_W1,own_down=own_down_post,own_up=own_up_post,
          alpha=alpha,delta=delta, cost_w =c_W_vec,
          cost_r =c_R_vec, lambda=0.5, price_r = p_R1)
#> [1] 0.0001292351


# Test that foc_vert gives same result as before when VI = 0.
mergersim:::bargain_foc_novert_sim(price_w = p_W1, own_down = own_down_pre, own_up = own_up_pre, 
         alpha= alpha, delta = delta, cost_w = c_W_vec, 
         cost_r = c_R_vec, lambda = lambda, price_r = p_R1)
#> [1] 1.742127e-08

bargain_foc_vert_sim(price_w = p_W1,own_down=own_down_pre,own_up=own_up_pre,
          alpha=alpha,delta=delta, cost_w =c_W_vec,
          cost_r =c_R_vec, lambda=0.5, price_r = p_R1)
#> [1] 1.742127e-08

# And for upstream
bertrand_foc_vert(price_r = p_R1, own_down = own_down_pre, own_up = own_up_pre,
          alpha = alpha, delta = delta,
          cost_r = c_R_vec, price_w = p_W1, 
          cost_w = c_W_vec)
#>             [,1]
#> R1  2.892024e-05
#> R1  1.532181e-05
#> R2  1.024178e-05
#> R2  2.737772e-05
#> R3 -4.687353e-06
#> R3  5.286390e-06

mergersim:::bertrand_foc_novert(price_r = p_R1, own_down = own_down_pre, alpha = alpha, 
         delta = delta, cost = c_R_vec, price_w = p_W1)
#>             [,1]
#> R1  2.892024e-05
#> R1  1.532181e-05
#> R2  1.024178e-05
#> R2  2.737772e-05
#> R3 -4.687353e-06
#> R3  5.286390e-06

Calibrating the demand parameters

Calibration takes place in two steps. First, we use the downstream model to calibrate demand parameters. Then we will use the upstream model to calibrate the bargaining weight. In this code, we require the assumption that pre-merger there is no vertical integration, and that all relevant costs are observed. It is, however, possible to calibrate the model using cost data from only one downstream and one upstream firm.

When all costs are observed

First we define a function to calibrate the downstream model of Bertrand competition.

Our goal is to see if we can recover the underlying demand parameters based on the observed market shares and retail prices. Note that costs in the downstream include both retailer costs and the wholesale price paid by the retailer for the goods.

J <- length(p_R1)
alpha_start <- -1
delta_start <- rep(0.5,J)
x00b <- -1.1
wt_matrix <- diag(c(rep(1,J),rep(10,J)))

c_j <- c_R_vec  # eventually remove c_j from code

out3 <- BBoptim(f = bertrand_vert_calibrate, par = x00b, 
                   own_down = own_down_pre, price = p_R1, 
                   shares = shares1, cost  = c_j, price_w = p_W1)

alpha3 <- out3$par
delta3 <- log(shares1) - log(1-sum(shares1)) - alpha3*p_R1

# recover true parameters
alpha
#> [1] -0.9
delta
#> [1] 0.2 0.3 0.9 1.0 0.8 0.9
alpha3
#> [1] -0.8999888
delta3
#> [1] 0.1999670 0.2999666 0.8999656 0.9999653 0.7999658 0.8999655

The calibrated demand parameters match the true parameter values.

# note that this optimization recovers the true demand parameters and shares

shares3 <- (exp(delta3 + alpha3*p_R1))/(1+sum(exp(delta3 + alpha3*p_R1)))
shares3
#> [1] 0.04798858 0.05155402 0.08643620 0.09306666 0.07977231 0.08586495
as.numeric(shares1)
#> [1] 0.04798858 0.05155402 0.08643620 0.09306666 0.07977231 0.08586495

Next, we calibrate the upstream market, which in particular requires recovering the bargaining weight. If all relevant costs are observed, the only remaining parameter to recover is the bargaining weight, which we can recover from the gains from trade of the bargaining model. First we define the calibration function bargain_vert_sim_calibrate.

And next see if we can recover the true bargaining weight

lambda_start <- 0.4   # starting value for lambda 

out4 <- BBoptim(f = bargain_vert_sim_calibrate, par = lambda_start, 
                price_w = p_W1,own_down = own_down_pre,
                own_up =own_up_pre,alpha=alpha3,delta=delta3,
                cost_w = c_W_vec, cost_r = c_R_vec, price_r = p_R1,
                lower = 0, upper = 1)
lambda_end <- out4$par
lambda_end
#> [1] 0.500313

When some costs are unobserved

The functions above assume all costs are observed. Functions that end with the number 2 are modified versions that can accept missing costs.

Suppose that we only observed costs from one downstream firm. We can use these functions to still recover the true demand parameters.

# Set some costs to NA -- (this line not fully general yet)
c_R_vec_NA <- c(c_R_vec[own_down_pre == "R1"], NA, NA, NA, NA)


x00b <- -1.1
bertrand_vert_calibrate(param = x00b, 
                   own_down = own_down_pre, price = p_R1, 
                   shares = shares1, cost  = c_R_vec_NA, price_w = p_W1 )
#> iter:  0  f-value:  0.0002437925  pgrad:  0.001064769
#>            [,1]
#> [1,] 0.07958337


out3d <- BBoptim(f = bertrand_vert_calibrate, par = x00b, 
                   own_down = own_down_pre, price = p_R1, 
                   shares = shares1, cost  = c_R_vec_NA, price_w = p_W1)
#> iter:  0  f-value:  0.0002437925  pgrad:  0.001064769 
#> iter:  0  f-value:  0.0002437922  pgrad:  0.001064768 
#> iter:  0  f-value:  0.07958337  pgrad:  0.6490849 
#> iter:  0  f-value:  0.003920012  pgrad:  0.0008117139 
#> iter:  0  f-value:  0.0005521595  pgrad:  0.001350247 
#> iter:  0  f-value:  1.554035e-05  pgrad:  0.0002636499 
#> iter:  0  f-value:  1.554041e-05  pgrad:  0.0002636504 
#> iter:  0  f-value:  9.557828e-06  pgrad:  0.0002122097 
#> iter:  0  f-value:  9.557779e-06  pgrad:  0.0002122092 
#> iter:  0  f-value:  2.729944e-07  pgrad:  3.622622e-05 
#> iter:  0  f-value:  2.729862e-07  pgrad:  3.622568e-05 
#> iter:  0  f-value:  3.22003e-09  pgrad:  3.959217e-06 
#> iter:  0  f-value:  3.220901e-09  pgrad:  3.959677e-06 
#>   Successful convergence.

alpha3d <- out3d$par
delta3d <- log(shares1) - log(1-sum(shares1)) - alpha3d*p_R1

# still recover true parameters
alpha
#> [1] -0.9
delta
#> [1] 0.2 0.3 0.9 1.0 0.8 0.9
alpha3d
#> [1] -0.8996829
delta3d
#> [1] 0.1990669 0.2990569 0.8990276 0.9990184 0.7990346 0.8990253

Then one can use the demand parameters and first order conditions to back out the implied values for the missing cost information.



x00b <- rep(1,J)
mergersim:::bertrand_vert_calibrate_costs(param = x00b, 
                   own_down = own_down_pre, price = p_R1, 
                   shares = shares1, alpha = alpha, delta = delta,
                   price_w = p_W1 )


out3e <- BBoptim(f = mergersim:::bertrand_vert_calibrate_costs, par = x00b, 
                   own_down = own_down_pre, price = p_R1, 
                   shares = shares1, alpha = alpha, delta = delta,
                   price_w = p_W1)
out3e$par
#> [1] 0.09860285 0.09984074 0.09989396 0.09933240 0.10118727 0.10104626
c(c_R_vec)
#> [1] 0.1 0.1 0.1 0.1 0.1 0.1

On model fit

When data is inconsistent with this model, even the best fitting calibrated demand parameters will not perfectly match the model prices and shares to the observed prices and shares.

Note that in this case, when data is generated from the model, there is a near perfect positive correlation between downstream prices and downstream margins. If data being input into this model does not exhibit a positive correlation between downstream margins and shares, this model will have a difficult time calibrating parameters to match the data.

shares1
#> [1] 0.04798858 0.05155402 0.08643620 0.09306666 0.07977231 0.08586495
m_downstream <- p_R1 - c_R_vec - p_W1

cor(shares1,m_downstream)
#>           [,1]
#> [1,] 0.9872536
# plot(shares1,m_downstream)

Predicting Merger Effects

Once the true underlying parameters have been recovered, the ownership matrices can be changed to predict the effects of vertical integration in the market.

# ( Ideally, first recover pre-merger prices, using VI functions.)

bertrand_foc_vert(price_r=p_R1,own_down=own_down_post,own_up=own_up_post,
          alpha=alpha,delta=delta,
          cost_r=c_R_vec,price_w=p_W1, cost_w=c_W_vec)

bargain_foc_vert_sim(price_w=p_W1,own_down = own_down_post,own_up=own_up_post,
          alpha=alpha,delta=delta, cost_w=c_W_vec,
          cost_r=c_R_vec, lambda=0.5, price_r = p_R1)

tol <- .0001
error <- 1

p_W0_post <- matrix(.25, nrow = (R*W), ncol = 1)  # dummy wholesale prices
p_R0_post <- p_r_start
  
while (error > tol) {

  # Use EITHER multiroot or BBoptim
  # multiroot:
  # out1 <- multiroot(f = bertrand_foc_vert ,start = p_R0_post, 
  #                 own_down = own_down_post, own_up = own_up_post,
  #                 alpha = alpha3, delta = delta3, c_R = c_R_vec, 
  #                 p_W = p_W0_post, c_W = c_W_vec)
  # 
  # p_R1_post <- out1$root
  # BBoptim:
  out1 <- BBoptim(f = bertrand_foc_vert, par = p_R0_post, 
                  own_down = own_down_post, own_up = own_up_post,
                  alpha = alpha3, delta = delta3, cost_r = c_R_vec, 
                  price_w = p_W0_post, cost_w = c_W_vec, sumFOC = TRUE)

  p_R1_post <- out1$par
  
  out2 <- BBoptim(par = as.numeric(p_W0_post), fn = bargain_foc_vert_sim, 
                  own_down = own_down_post, own_up = own_up_post, 
                  alpha= alpha3, delta = delta3, 
                  cost_w = c_W_vec, cost_r = c_R_vec, lambda = lambda_end, 
                  price_r = p_R1_post)
  
  p_W1_post <- out2$par
  
  error <- max(abs(c(p_W1_post-p_W0_post,p_R1_post-p_R0_post)))
  print(error)
  
  p_W0_post <- p_W1_post
  p_R0_post <- p_R1_post
}
p_R1
#> [1] 2.942863 2.974344 3.066814 3.095804 3.044848 3.074182
p_W1
#> [1] 1.609646 1.640788 1.612805 1.641989 1.613095 1.642563
p_R1_post
#> [1] 1.876456 3.280460 3.206903 3.044468 3.186708 3.023596
p_W1_post
#> [1] 0.250000 1.606545 1.775417 1.613407 1.775412 1.613352

# retail price change with vertical integration
(p_R1_post-p_R1)/p_R1
#> [1] -0.36237072  0.10291877  0.04567885 -0.01658255  0.04659025 -0.01645509

Comparison to ‘antitrust’ package

We can also show that the R package ‘antitrust’ can be used to recover the same true demand parameters.

shareDown <- as.numeric(shares1)
priceDown <- as.numeric(p_R1)
ownerPreDown <- own_down_pre
priceUp <- as.numeric(p_W1)
ownerPreUp <- own_up_pre
priceOutSide <- 0

# Downstream margin as a percent
marginDown <- (priceDown - c_R_vec - priceUp)/ priceDown
# Upstream margin as a percent
marginUp <- (priceUp - c_W_vec) / priceUp

## Simulate a vertical merger
ownerPostUp <- own_up_post
ownerPostDown <- own_down_post

simres_vert <- vertical.barg(sharesDown =shareDown,
                             pricesDown = priceDown,
                             marginsDown = as.numeric(marginDown),
                             ownerPreDown = ownerPreDown,
                             ownerPostDown = ownerPostDown,
                             pricesUp = priceUp,
                             marginsUp = as.numeric(marginUp),
                             ownerPreUp = ownerPreUp,
                             ownerPostUp = ownerPostUp,
                             priceOutside = priceOutSide)

summary(simres_vert)
#> 
#> Merger simulation results under 'VertBargBertLogit' demand:
#> 
#>         priceUpPre priceUpPost priceUpDelta priceDownPre priceDownPost priceDownDelta sharesPre
#> * Prod1        1.6         0.2        -87.7          2.9           1.9          -36.4        10
#> * Prod2        1.6         1.6         -1.8          3.0           3.3           10.5        11
#> * Prod3        1.6         1.8         10.1          3.1           3.2            4.5        20
#>   Prod4        1.6         1.6         -1.8          3.1           3.0           -1.7        21
#> * Prod5        1.6         1.8         10.1          3.0           3.2            4.6        18
#>   Prod6        1.6         1.6         -1.8          3.1           3.0           -1.6        19
#>         sharesPost outputDelta
#> * Prod1       16.8       61.31
#> * Prod2        9.1      -19.35
#> * Prod3       17.5      -10.77
#>   Prod4       21.2       -0.30
#> * Prod5       16.0      -10.85
#>   Prod6       19.4       -0.43
#> 
#>  Notes: '*' indicates merging parties' products.
#>      Deltas are percent changes.
#>  Output is based on revenues.

simres_vert@down@slopes
#> $alpha
#> [1] -0.9001873
#> 
#> $meanval
#> [1] 0.2005513 0.3005572 0.9005745 1.0005800 0.8005704 0.9005759
#> 
#> $sigma
#> [1] 0.1

(simres_vert@down@pricePost - simres_vert@down@pricePre) / simres_vert@down@pricePre
#> [1] -0.36360082  0.10537542  0.04533485 -0.01696478  0.04634040 -0.01641208