25. The Cobweb Model#

The cobweb model is a model of prices and quantities in a given market, and how they evolve over time.

25.1. Overview#

The cobweb model dates back to the 1930s and, while simple, it remains significant because it shows the fundamental importance of expectations.

To give some idea of how the model operates, and why expectations matter, imagine the following scenario.

There is a market for soybeans, say, where prices and traded quantities depend on the choices of buyers and sellers.

The buyers are represented by a demand curve — they buy more at low prices and less at high prices.

The sellers have a supply curve — they wish to sell more at high prices and less at low prices.

However, the sellers (who are farmers) need time to grow their crops.

Suppose now that the price is currently high.

Seeing this high price, and perhaps expecting that the high price will remain for some time, the farmers plant many fields with soybeans.

Next period the resulting high supply floods the market, causing the price to drop.

Seeing this low price, the farmers now shift out of soybeans, restricting supply and causing the price to climb again.

You can imagine how these dynamics could cause cycles in prices and quantities that persist over time.

The cobweb model puts these ideas into equations so we can try to quantify them, and to study conditions under which cycles persist (or disappear).

In this lecture, we investigate and simulate the basic model under different assumptions regarding the way that producers form expectations.

Our discussion and simulations draw on high quality lectures by Cars Hommes.

We will use the following imports.

import numpy as np
import matplotlib.pyplot as plt

25.2. History#

Early papers on the cobweb cycle include [Waugh, 1964] and [Harlow, 1960].

The paper [Harlow, 1960] uses the cobweb theorem to explain the prices of hog in the US over 1920–1950.

The next plot replicates part of Figure 2 from that paper, which plots the price of hogs at yearly frequency.

Notice the cyclical price dynamics, which match the kind of cyclical soybean price dynamics discussed above.

hog_prices = [55, 57, 80, 70, 60, 65, 72, 65, 51, 49, 45, 80, 85,
              78, 80, 68, 52, 65, 83, 78, 60, 62, 80, 87, 81, 70,
              69, 65, 62, 85, 87, 65, 63, 75, 80, 62]
years = np.arange(1924, 1960)
fig, ax = plt.subplots()
ax.plot(years, hog_prices, '-o', ms=4, label='hog price')
ax.set_xlabel('year')
ax.set_ylabel('dollars')
ax.legend()
ax.grid()
plt.show()
_images/673513764b4661a5ec91693f94fb36eeed4a3ffd57e8fe6e806b5693d40b9e76.png

25.3. The model#

Let’s return to our discussion of a hypothetical soybean market, where price is determined by supply and demand.

We suppose that demand for soybeans is given by

\[ D(p_t) = a - b p_t \]

where \(a, b\) are nonnegative constants and \(p_t\) is the spot (i.e, current market) price at time \(t\).

(\(D(p_t)\) is the quantity demanded in some fixed unit, such as thousands of tons.)

Because the crop of soybeans for time \(t\) is planted at \(t-1\), supply of soybeans at time \(t\) depends on expected prices at time \(t\), which we denote \(p^e_t\).

We suppose that supply is nonlinear in expected prices, and takes the form

\[ S(p^e_t) = \tanh(\lambda(p^e_t - c)) + d \]

where \(\lambda\) is a positive constant, \(c, d\) are nonnegative constants and \(\tanh\) is a type of hyperbolic function.

Let’s make a plot of supply and demand for particular choices of the parameter values.

First we store the parameters in a class and define the functions above as methods.

class Market:

    def __init__(self,
                 a=8,      # demand parameter
                 b=1,      # demand parameter
                 c=6,      # supply parameter
                 d=1,      # supply parameter
                 λ=2.0):   # supply parameter
        self.a, self.b, self.c, self.d = a, b, c, d
        self.λ = λ

    def demand(self, p):
        a, b = self.a, self.b
        return a - b * p

    def supply(self, p):
        c, d, λ = self.c, self.d, self.λ
        return np.tanh(λ * (p - c)) + d

Now let’s plot.

p_grid = np.linspace(5, 8, 200)
m = Market()
fig, ax = plt.subplots()

ax.plot(p_grid, m.demand(p_grid), label="$D$")
ax.plot(p_grid, m.supply(p_grid), label="$S$")
ax.set_xlabel("price")
ax.set_ylabel("quantity")
ax.legend()

plt.show()
_images/2aee4a23f0e13207475854ef6f61df21591bc3501572459cd8b6499f28ee133a.png

Market equilibrium requires that supply equals demand, or

\[ a - b p_t = S(p^e_t) \]

Rewriting in terms of \(p_t\) gives

\[ p_t = - \frac{1}{b} [S(p^e_t) - a] \]

Finally, to complete the model, we need to describe how price expectations are formed.

We will assume that expected prices at time \(t\) depend on past prices.

In particular, we suppose that

(25.1)#\[ p^e_t = f(p_{t-1}, p_{t-2})\]

where \(f\) is some function.

Thus, we are assuming that producers expect the time-\(t\) price to be some function of lagged prices, up to \(2\) lags.

(We could of course add additional lags and readers are encouraged to experiment with such cases.)

Combining the last two equations gives the dynamics for prices:

(25.2)#\[ p_t = - \frac{1}{b} [ S(f(p_{t-1}, p_{t-2})) - a]\]

The price dynamics depend on the parameter values and also on the function \(f\) that determines how producers form expectations.

25.4. Naive expectations#

To go further in our analysis we need to specify the function \(f\); that is, how expectations are formed.

Let’s start with naive expectations, which refers to the case where producers expect the next period spot price to be whatever the price is in the current period.

In other words,

\[ p_t^e = p_{t-1} \]

Using (25.2), we then have

\[ p_t = - \frac{1}{b} [ S(p_{t-1}) - a] \]

We can write this as

\[ p_t = g(p_{t-1}) \]

where \(g\) is the function defined by

(25.3)#\[ g(p) = - \frac{1}{b} [ S(p) - a]\]

Here we represent the function \(g\)

def g(model, current_price):
    """
    Function to find the next price given the current price
    and Market model
    """
    a, b = model.a, model.b
    next_price = - (model.supply(current_price) - a) / b
    return next_price

Let’s try to understand how prices will evolve using a 45-degree diagram, which is a tool for studying one-dimensional dynamics.

The function plot45 defined below helps us draw the 45-degree diagram.

Hide code cell source
def plot45(model, pmin, pmax, p0, num_arrows=5):
    """
    Function to plot a 45 degree plot

    Parameters
    ==========

    model: Market model

    pmin: Lower price limit

    pmax: Upper price limit

    p0: Initial value of price (needed to simulate prices)

    num_arrows: Number of simulations to plot
    """
    pgrid = np.linspace(pmin, pmax, 200)

    fig, ax = plt.subplots()
    ax.set_xlim(pmin, pmax)
    ax.set_ylim(pmin, pmax)

    hw = (pmax - pmin) * 0.01
    hl = 2 * hw
    arrow_args = dict(fc="k", ec="k", head_width=hw,
            length_includes_head=True, lw=1,
            alpha=0.6, head_length=hl)

    ax.plot(pgrid, g(model, pgrid), 'b-',
            lw=2, alpha=0.6, label='g')
    ax.plot(pgrid, pgrid, lw=1, alpha=0.7, label='$45\degree$')

    x = p0
    xticks = [pmin]
    xtick_labels = [pmin]

    for i in range(num_arrows):
        if i == 0:
            ax.arrow(x, 0.0, 0.0, g(model, x),
                     **arrow_args)
        else:
            ax.arrow(x, x, 0.0, g(model, x) - x,
                     **arrow_args)
            ax.plot((x, x), (0, x), ls='dotted')

        ax.arrow(x, g(model, x),
                 g(model, x) - x, 0, **arrow_args)
        xticks.append(x)
        xtick_labels.append(r'$p_{}$'.format(str(i)))

        x = g(model, x)
        xticks.append(x)
        xtick_labels.append(r'$p_{}$'.format(str(i+1)))
        ax.plot((x, x), (0, x), '->', alpha=0.5, color='orange')

    xticks.append(pmax)
    xtick_labels.append(pmax)
    ax.set_ylabel(r'$p_{t+1}$')
    ax.set_xlabel(r'$p_t$')
    ax.set_xticks(xticks)
    ax.set_yticks(xticks)
    ax.set_xticklabels(xtick_labels)
    ax.set_yticklabels(xtick_labels)

    bbox = (0., 1.04, 1., .104)
    legend_args = {'bbox_to_anchor': bbox, 'loc': 'upper right'}

    ax.legend(ncol=2, frameon=False, **legend_args, fontsize=14)
    plt.show()
<>:32: SyntaxWarning: invalid escape sequence '\d'
<>:32: SyntaxWarning: invalid escape sequence '\d'
/tmp/ipykernel_5297/2076687334.py:32: SyntaxWarning: invalid escape sequence '\d'
  ax.plot(pgrid, pgrid, lw=1, alpha=0.7, label='$45\degree$')

Now we can set up a market and plot the 45-degree diagram.

m = Market()
plot45(m, 0, 9, 2, num_arrows=3)
_images/ff788d2a0cefea7ea11c3be6da09ba6b815e818e1bce649bcaf3789d15f3b89c.png

The plot shows the function \(g\) defined in (25.3) and the 45-degree line.

Think of \( p_t \) as a value on the horizontal axis.

Since \(p_{t+1} = g(p_t)\), we use the graph of \(g\) to see \(p_{t+1}\) on the vertical axis.

Clearly,

  • If \( g \) lies above the 45-degree line at \(p_t\), then we have \( p_{t+1} > p_t \).

  • If \( g \) lies below the 45-degree line at \(p_t\), then we have \( p_{t+1} < p_t \).

  • If \( g \) hits the 45-degree line at \(p_t\), then we have \( p_{t+1} = p_t \), so \( p_t \) is a steady state.

Consider the sequence of prices starting at \(p_0\), as shown in the figure.

We find \(p_1\) on the vertical axis and then shift it to the horizontal axis using the 45-degree line (where values on the two axes are equal).

Then from \(p_1\) we obtain \(p_2\) and continue.

We can see the start of a cycle.

To confirm this, let’s plot a time series.

def ts_plot_price(model,             # Market model
                  p0,                # Initial price
                  y_a=3, y_b= 12,    # Controls y-axis
                  ts_length=10):     # Length of time series
    """
    Function to simulate and plot the time series of price.

    """
    fig, ax = plt.subplots()
    ax.set_xlabel(r'$t$', fontsize=12)
    ax.set_ylabel(r'$p_t$', fontsize=12)
    p = np.empty(ts_length)
    p[0] = p0
    for t in range(1, ts_length):
        p[t] = g(model, p[t-1])
    ax.plot(np.arange(ts_length),
            p,
            'bo-',
            alpha=0.6,
            lw=2,
            label=r'$p_t$')
    ax.legend(loc='best', fontsize=10)
    ax.set_ylim(y_a, y_b)
    ax.set_xticks(np.arange(ts_length))
    plt.show()
ts_plot_price(m, 4, ts_length=15)
_images/fe2fff3941b80a24f351f80d2bc2bf01c22986dfdcbd7a55af6c7aa0d1083e57.png

We see that a cycle has formed and the cycle is persistent.

(You can confirm this by plotting over a longer time horizon.)

The cycle is “stable”, in the sense that prices converge to it from most starting conditions.

For example,

ts_plot_price(m, 10, ts_length=15)
_images/60b5f076dc3ec2360563e13f989f17c756bc21af614fef9d175448cfb08994b5.png

25.5. Adaptive expectations#

Naive expectations are quite simple and also important in driving the cycle that we found.

What if expectations are formed in a different way?

Next we consider adaptive expectations.

This refers to the case where producers form expectations for the next period price as a weighted average of their last guess and the current spot price.

That is,

(25.4)#\[p_t^e = \alpha p_{t-1} + (1-\alpha) p^e_{t-1} \qquad (0 \leq \alpha \leq 1)\]

Another way to write this is

(25.5)#\[p_t^e = p^e_{t-1} + \alpha (p_{t-1} - p_{t-1}^e)\]

This equation helps to show that expectations shift

  1. up when prices last period were above expectations

  2. down when prices last period were below expectations

Using (25.4), we obtain the dynamics

\[ p_t = - \frac{1}{b} [ S(\alpha p_{t-1} + (1-\alpha) p^e_{t-1}) - a] \]

Let’s try to simulate the price and observe the dynamics using different values of \(\alpha\).

def find_next_price_adaptive(model, curr_price_exp):
    """
    Function to find the next price given the current price expectation
    and Market model
    """
    return - (model.supply(curr_price_exp) - model.a) / model.b

The function below plots price dynamics under adaptive expectations for different values of \(\alpha\).

def ts_price_plot_adaptive(model, p0, ts_length=10, α=[1.0, 0.9, 0.75]):
    fig, axs = plt.subplots(1, len(α), figsize=(12, 5))
    for i_plot, a in enumerate(α):
        pe_last = p0
        p_values = np.empty(ts_length)
        p_values[0] = p0
        for i in range(1, ts_length):
            p_values[i] = find_next_price_adaptive(model, pe_last)
            pe_last = a*p_values[i] + (1 - a)*pe_last

        axs[i_plot].plot(np.arange(ts_length), p_values)
        axs[i_plot].set_title(r'$\alpha={}$'.format(a))
        axs[i_plot].set_xlabel('t')
        axs[i_plot].set_ylabel('price')
    plt.show()

Let’s call the function with prices starting at \(p_0 = 5\).

ts_price_plot_adaptive(m, 5, ts_length=30)
_images/764627a5c42d6930e303c274acde3bc3d2a1f9ac60a0642fd437256d847cff46.png

Note that if \(\alpha=1\), then adaptive expectations are just naive expectation.

Decreasing the value of \(\alpha\) shifts more weight to the previous expectations, which stabilizes expected prices.

This increased stability can be seen in the figures.

25.6. Exercises#

Exercise 25.1

Using the default Market class and naive expectations, plot a time series simulation of supply (rather than the price).

Show, in particular, that supply also cycles.

Exercise 25.2

Backward looking average expectations

Backward looking average expectations refers to the case where producers form expectations for the next period price as a linear combination of their last guess and the second last guess.

That is,

(25.6)#\[p_t^e = \alpha p_{t-1} + (1-\alpha) p_{t-2}\]

Simulate and plot the price dynamics for \(\alpha \in \{0.1, 0.3, 0.5, 0.8\}\) where \(p_0=1\) and \(p_1=2.5\).