Cournot: Solving for a Nash Equilibrium

Several firms each choose how much to produce. The market price falls with the average quantity supplied, so a firm’s best output depends on what the other firms choose, and their best outputs depend in turn on its choice.

Some games can be solved by decomposing them: their decisions fall into an order, and each one can be settled once the decisions it relies on have been settled. This game admits no such order, because there is no decision here that can be settled first. What it has instead is a fixed point, a quantity that is its own best response; that quantity is the Cournot-Nash equilibrium.

This page does four things:

  1. states the model and the three quantities worth knowing about it,

  2. projects the population onto one firm and its rivals,

  3. iterates best responses to the equilibrium, and

  4. shows why the iteration has to be damped, by watching it fail without it.

The Model

Cournot (1838) [1] describes \(N\) firms selling one identical good. Each firm draws a marginal cost and chooses a quantity, the quantities together determine the price, and each firm is paid at that common price.

Notation

  • Shock: \(c_i\), firm \(i\)’s marginal cost, drawn uniformly on \([c_l, c_h]\). The calibration used below gives every firm the same cost \(c\), which is what makes the equilibrium symmetric.

  • Control: \(q_i\), firm \(i\)’s quantity, the one decision it makes.

  • Aggregate: \(Q\), the average quantity. It is the only quantity that leaves the firm class, and it is what couples the firms to each other.

  • Price: \(P\), set by inverse demand from \(Q\).

  • Payoff: \(u_i\), firm \(i\)’s profit.

\[Q = \frac{1}{N}\sum_j q_j, \qquad P = A - b\,Q, \qquad u_i = (P - c_i)\, q_i\]

Demand is written against the average rather than the total, so \(b\) is the slope on the average and \(b/N\) is the slope on total quantity. Both readings of \(N\) then live in one model: it is the size of the population and it is a parameter of demand, and neither reading needs an extra equation.

The model is static, since it has no arrival states, so solving it describes one play of a one-shot game.

The best response

Firm \(i\) takes the other firms’ quantities as given. Writing \(\bar q\) for the quantity each of the other \(N - 1\) firms produces, its profit is

\[u_i = \left(A - \frac{b}{N}\bigl(q_i + (N-1)\bar q\bigr) - c_i\right) q_i\]

which is concave in \(q_i\), so the first-order condition

\[A - c_i - \frac{b}{N}\bigl(2 q_i + (N-1)\bar q\bigr) = 0\]

gives the best response

\[q_i^{\ast}(\bar q) = \frac{1}{2}\left(\frac{N (A - c_i)}{b} - (N-1)\,\bar q\right)\]

The slope of that line in \(\bar q\) is \(-(N-1)/2\). It is the single most important number on this page: it is negative, so the firms’ quantities are strategic substitutes, and its magnitude passes 1 at \(N = 3\), which is where iterating best responses stops working and the damping of section 4 becomes necessary.

The equilibrium, and the outcome the firms would prefer

At a symmetric equilibrium every firm plays the same \(q^{\ast}\), so setting \(q_i = \bar q = q^{\ast}\) above and solving gives the Cournot-Nash quantity, while maximizing the firms’ joint profit instead gives the monopoly quantity they would rather share:

\[q^{\ast} = \frac{N (A - c)}{b\,(N + 1)}, \qquad q^{m} = \frac{A - c}{2 b}\]

At the calibration used here – \(A = 10\), \(b = 1\), \(c = 4\) and \(N = 3\) – those are \(q^{\ast} = 4.5\) and \(q^{m} = 3\). The three profiles printed below are exactly these two and one deviation from the second, and every number in them follows from the equations above:

profile

per firm

\(Q\)

\(P\)

payoff

Cournot-Nash

4.5

4.5

5.5

6.75 each

joint monopoly

3.0

3.0

7.0

9.0 each

one firm deviates

6.0 against 3.0

4.0

6.0

12.0 to the deviator, 6.0 to the others

The deviation is the best response to a cartel: \(q^{\ast}(3.0) = \tfrac{1}{2}(3 \cdot 6 - 2 \cdot 3) = 6.0\). That is why the cartel is not an equilibrium and 4.5 is.

References

import numpy as np

import skagent.models.cournot as cournot
from skagent.ground import GroundedBlock
from skagent.solver import ExactBestResponse, project, solve_symmetric_equilibrium
from skagent.utils import plot_block_diagram


COST = 4.0
market = GroundedBlock(cournot.cournot_block, cournot.collusion_calibration(size=3))

The model as a graph

The cost c is a shock, the quantity q is the firm’s decision, and u is the firm’s payoff. The one edge that leaves the firm class is Q, the average quantity, and it sets the price P that every firm is paid at. A single decision node stands for the whole class, which is what makes this a population model rather than a three-firm game written out three times.

plot_block_diagram(
    cournot.cournot_block,
    "Cournot: each firm's quantity feeds the average, which sets the price",
    calibration=market.calibration,
    figsize=(9, 4.5),
)
Cournot: each firm's quantity feeds the average, which sets the price

Three profiles, and why the firms do not reach the one they prefer

The model ships three hand-derived profiles. They are the prisoner’s dilemma in disguise, which is what makes the equilibrium worth computing rather than guessing, because the outcome the firms reach is not the outcome they would rank highest.

for label, quantities in cournot.PROFILES.items():
    print(f"{label:16s} {quantities}")

print()
print(f"joint monopoly, per firm : {cournot.monopoly_quantity()}  (pays 9.0 each)")
print(f"Cournot-Nash, per firm   : {cournot.nash_quantity()}  (pays 6.75 each)")
print("one firm deviating       : 6.0 against 3.0, and it pays 12.0, at the")
print("                           expense of the other two, which fall to 6.0")
cournot-nash     [4.5, 4.5, 4.5]
joint-monopoly   [3.0, 3.0, 3.0]
one-defects      [6.0, 3.0, 3.0]

joint monopoly, per firm : 3.0  (pays 9.0 each)
Cournot-Nash, per firm   : 4.5  (pays 6.75 each)
one firm deviating       : 6.0 against 3.0, and it pays 12.0, at the
                           expense of the other two, which fall to 6.0

Colluding at 3.0 pays every firm more than the equilibrium does, but it is not stable, because any single firm gains by producing more. That is why 4.5 is where the market lands. The equilibrium is a prediction rather than a recommendation.

Whose ranking this is matters. The cartel is preferred by the firms, and only by the firms: it pays them 9.0 each by holding output down and the price up, which is the same thing as charging buyers more for less. Nothing computed on this page speaks to which outcome is better overall, since the model carries the firms’ payoffs and no one else’s.

Projecting the population

A solver solves one decision. This model describes three firms at once, so something has to turn the question “what should the firms do?” into the question “what should this firm do, given what the others do?” That is the job of project().

projected = project(market)

plot_block_diagram(
    projected.block,
    "Projected: one firm's decision beside the rest of its class",
    calibration=projected.calibration,
    figsize=(9, 5),
)
Projected: one firm's decision beside the rest of its class

The class has been split into the firm being solved (_actor) and the rest of the firms (_other), and one equation has been synthesized. That equation, q, concatenates the two sides back into the population the market reads.

That is the whole trick, and it is worth being precise about what it avoids. The projection did not rewrite Q = q.mean() into a formula about one firm and \(N-1\) others. It reassembled the population and let the model’s own equation run on it unchanged. So the solved firm’s own share of the aggregate, which is its \(1/N\) of the average, is there by construction rather than being computed, and a model that had written q.sum() or a maximum would project just as well without the library knowing which reduction was used.

We can check the projection against the profiles above before solving anything: put the deviating firm at 6.0 with its rivals at 3.0, and it should earn the 12.0 the table promises.

values = projected.block.transition(
    {**projected.calibration, "c_actor": COST, "c_other": COST},
    {"q_actor": lambda c_actor: 6.0, "q_other": lambda c_other: 3.0},
)
payoff = projected.block.calc_reward(values, agent="firm_actor")["u_actor"]
print(f"deviator's payoff: {float(np.atleast_1d(payoff)[0])}")
deviator's payoff: 12.0

Iterating to the equilibrium

We now come to the fixed point. solve_symmetric_equilibrium() solves the projected firm’s decision against the rivals’ current rule, substitutes the answer back in as the rivals’ rule, and repeats until the rule stops moving.

The method supplying each solve is an exact backup here. A policy network would serve just as well, and it reaches the same answer.

def cournot_method(size):
    ground = GroundedBlock(
        cournot.cournot_block, cournot.collusion_calibration(size=size)
    )
    projected = project(ground)
    return ExactBestResponse(
        projected,
        {"c_actor": np.array([COST])},
        scope={**projected.calibration, "c_other": COST},
    )


def quantity(rule):
    return float(np.atleast_1d(rule(np.array([COST]))).ravel()[0])


for size in (2, 3, 4):
    rule, info = solve_symmetric_equilibrium(
        cournot_method(size), damping=2.0 / (size + 1)
    )
    print(
        f"{size} firms: q* = {quantity(rule):.4f}   "
        f"analytic {cournot.nash_quantity(size=size)}   "
        f"({info['iterations']} iterations)"
    )
2 firms: q* = 4.0000   analytic 4.0   (2 iterations)
3 firms: q* = 4.5000   analytic 4.5   (2 iterations)
4 firms: q* = 4.8000   analytic 4.8   (2 iterations)

Why the iteration is damped

damping moves the rule only part of the way toward the best response each round:

\[q_{k+1} = (1 - L)\, q_k + L \cdot \mathrm{BR}(q_k)\]

Damping cannot change the answer, because a rule that equals its own damped update also equals its own best response, so it is not a tuning control for accuracy. It is what makes the iteration arrive at all.

A Cournot best response slopes down: if the rivals produce more, this firm produces less, with slope \(-(N-1)/2\). Beyond two firms that response overshoots, and the undamped iteration does not settle. The runs below show it failing:

for size, cap in ((2, 30), (3, 30), (4, 8)):
    _, info = solve_symmetric_equilibrium(
        cournot_method(size), damping=1.0, max_iterations=cap
    )
    moves = [round(d, 2) for d in info["distances"][:6]]
    print(f"{size} firms undamped: converged={info['converged']}  first moves {moves}")
2 firms undamped: converged=True  first moves [6.0, 3.0, 1.5, 0.75, 0.37, 0.19]
3 firms undamped: converged=False  first moves [9.0, 9.0, 9.0, 9.0, 9.0, 9.0]
4 firms undamped: converged=False  first moves [12.0, 18.0, 27.0, 40.5, 60.75, 91.12]

There are three different failures here, and only the first one is benign:

  • Two firms: the slope is \(-0.5\), a contraction. It does converge, halving its step each round, but it takes 14 iterations where the damped iteration takes 2.

  • Three firms: the slope is exactly \(-1\). The iteration does not diverge; it cycles, alternating between two quantities forever. Every round moves by the same 9.0, and no iteration cap will change that.

  • Four firms: the slope is \(-1.5\). The moves grow, and the iterates run off to whatever bounds the control declares.

The middle case is the one to remember. A cycle returns a perfectly plausible quantity if it is stopped on a count of iterations, which is why the schedule tests whether the rule has stopped moving and reports converged: False rather than handing back its last iterate.

Note

\(L = 2/(N+1)\) is the damping that zeroes the slope for this model, which is why the runs above converge in a single step. That closed form exists because Cournot’s best response is linear; in general a damping factor is chosen to make the iteration a contraction, not to solve it instantly.

Total running time of the script: (0 minutes 0.492 seconds)

Gallery generated by Sphinx-Gallery