15. DSGE Models: The Frontier of Business Cycle Research

Mind map of Chapter 15: DSGE Models: The Frontier of Business Cycle Research

Open In Colab Open Lecture Slides Open Data Apps Open Model Apps

Learning goals

Research on business cycles today uses DSGE models: dynamic, stochastic, general equilibrium models. This chapter looks at where they came from and solves their most important building block, the labor market. By the end of it you will be able to:

  • explain the letters D, S and GE, the building blocks of a DSGE model (agents, shocks, frictions), and how real business cycle models grew into DSGE models;
  • derive the impulse response and the half-life of a persistent shock, and derive TFP growth as a Solow residual from the production function, including how changes in utilization bias it;
  • read data on total factor productivity (TFP) growth, for the United States and 120 other countries, and see why the way TFP is measured changes the answer to “do TFP shocks cause recessions?”;
  • derive labor supply \(L^s = \bar{\ell}\,w/c\) from a household’s utility, solve the labor market for employment and the real wage, read its comparative statics as elasticities, and use it to study a TFP shock, a sales tax, government purchases and good news;
  • show how sticky wages and sticky prices change the effects of monetary policy on employment and the real wage;
  • say what an impulse response function shows and what the estimated Smets–Wouters model reports;
  • in Python: write one equilibrium function and reuse it in many figures, reshape data with melt, draw a scatter in two panels with a regression line, and use period sliders and toggle buttons.

Model overview

Come back to Islandia, the small island country of earlier chapters, and watch it quarter by quarter. Every quarter the islanders decide how much to work and how much to consume, Harbor Boatworks decides how many of them to hire, and the Bank of Islandia sets its interest rate. And every quarter something unexpected can happen: a season in which every crew works less efficiently, a new tax, good news about the years ahead. Why does Islandia’s output rise in some quarters and fall in others, and why do some shocks fade within a year while others linger? Economists answer with a DSGE model, and this section builds its main pieces one at a time, following Islandia through each.

NoteThe big idea

The question. What makes an economy’s output, employment and wages move up and down from one quarter to the next, and how can a model say by how much?

The answer in one sentence. Random shocks hit an economy of households and firms that keep re-optimizing, and its frictions, such as adjustment costs and sticky wages or prices, decide how far and for how long each shock moves employment, wages and output.

What goes in and what comes out. We choose the shocks (their size and their persistence \(\rho\)), productivity, the sales tax, consumption, the willingness to work, and, for the new Keynesian blocks, the nominal wage, the price level or the output demanded. The model gives back impulse responses, employment and the real wage, output, and a way to measure productivity shocks in the data.

Road map.

  1. The building blocks of a DSGE model: the agents, shocks and frictions, the impulse response of a fading shock, and what such models deliver.
  2. Real business cycles: the Solow model with productivity shocks, and the Solow residual that measures them.
  3. The labor market of a DSGE model: labor demand and supply, solved by hand for employment and the real wage, and how each shock moves them.
  4. New Keynesian additions: sticky wages, sticky prices, and why monetary policy can now change employment.
  5. Model in code: the Key results as Python functions, checked with numbers. Every later section reuses these functions.

Notation. A bar over a letter marks a parameter, a number we take as given; letters without a bar are endogenous variables that the model determines (in the labor-market block, \(K\), \(\tau\) and \(c\) are also taken as given, although Jones writes them without a bar). In the code we simply write A, ell, c and tau.

Symbol Meaning Type
\(x_t\) percentage deviation of a shocked variable from its normal level endogenous
\(\rho\), \(\varepsilon_t\) persistence of a shock, and a new shock parameters
\(h\) quarters after the shock (the half-life when \(\rho^h = 1/2\)) notation
\(\lambda\) share of a gap closed each quarter (adjustment costs) parameter
\(Y_t, K_t, L_t\) output, capital and labor (hours) endogenous
\(A_t\), \(\bar{A}\) total factor productivity (TFP), and its normal level endogenous / parameter
\(g_Y, g_A, g_K, g_L, g_u\) growth rates of output, TFP, capital, labor and utilization notation
\(u_t\) utilization: how intensively workers and machines are used endogenous
\(w\), \(W\), \(P\) real wage \(W/P\), nominal wage, price level endogenous
\(\bar{W}\) a nominal wage that is stuck parameter
\(\tau\) tax on sales revenue parameter
\(c\), \(\bar{\ell}\) consumption per person, and the scale of labor supply parameters
\(\bar{S}\) a given amount of saving parameter
\(L^d, L^s\) labor demanded and labor supplied endogenous
\(L^*, w^*, Y^*\) a star marks the market-clearing equilibrium endogenous

The building blocks of a DSGE model

Definitions

The name lists what the model does: dynamic, today’s choices shape tomorrow; stochastic, random shocks keep arriving; general equilibrium, prices adjust until every market clears. It is assembled from three kinds of pieces.

1. Agents and the endogenous variables they decide. Each decision maker has a goal. On Islandia the cast is already familiar:

Agent On Islandia What it chooses What it wants
households the islanders consumption \(c\), saving, hours of work \(L\) the highest lifetime utility
firms Harbor Boatworks workers to hire, investment in capital the highest profit
government and central bank the Islandia Treasury, the Bank of Islandia purchases, taxes, the interest rate follow a policy rule

Their choices pin down GDP, consumption, investment, employment and unemployment, capital, wages, interest rates and inflation; bigger models add trade, debt or exchange rates. Almost every DSGE model has a labor market, because hours of work are a choice that moves production directly.

2. Shocks. The endogenous variables move because something hits the economy. The list is long: shocks to TFP, to monetary policy, to taxes and government purchases, to oil and other energy prices, to financial conditions, or to the markups that firms charge over their costs. We describe each shock along three lines:

  • size and persistence: is it temporary or permanent, and if temporary, does it die out fast or slowly?
  • timing: does it hit now, or do people learn now about something that will happen later (news)?
  • uncertainty: when the future is unclear, firms and households may hold back on hiring and investing, because waiting to see is worth something.

3. Features of the economy (frictions). Frictions decide how a shock travels through the economy over time:

  • nominal rigidities, that is, sticky wages or sticky prices. Without at least one of them, money cannot move real variables;
  • adjustment costs: changing the capital stock a lot, and quickly, is costly, so variables move gradually;
  • heterogeneity: households and firms are not all alike (for example, in the lowest wage at which they would take a job), and the total response depends on how they differ;
  • complete or incomplete markets: if every risk, such as losing one’s job, could be insured, shocks would be shared; when some insurance is missing, a shock lands much harder on some people than on others.

When one household stands in for everyone, we have a representative agent model. Models that keep track of differences between people, such as the heterogeneous agent new Keynesian (HANK) models, are at the research frontier.

A DSGE model has three ingredients: agents who optimize, shocks that hit them, and frictions that shape how the shocks spread.

NoteCheck your understanding

1. Sort these Islandian events into shocks and frictions: the Treasury raises the sales tax; Harbor Boatworks can change its prices only once a year; islanders hear that new fishing grounds will open in five years.

Answer

The tax rise is a shock (to taxes). The news about the fishing grounds is also a shock, a news shock: it changes what people know today about the future. The once-a-year price change is a friction, a nominal rigidity (sticky prices), which shapes how any shock spreads.

Solving the model

What solving a DSGE model means. In the Solow model, the saving rate and the hours of work were fixed numbers. In a DSGE model they are choices, made anew every quarter, and the choices are chained together: saving today becomes investment, investment becomes next quarter’s capital, capital changes next quarter’s wage, and that wage shapes the next choices of hours and consumption. Over 50 years, a quarterly model has \(50 \times 4 = 200\) periods. Every period each islander picks both consumption and hours, which makes 400 decisions, all linked to one another. Only a computer can solve that. So the chapter takes two shortcuts: it solves one block, the labor market, with pencil and paper (third block below), and it reads off the numerical results of a large estimated model (the notes after Figs. 3, 5 and 8).

How a shock fades: the impulse response. Let \(x_t\) be the percentage gap between a shocked variable and its normal level: if Islandia’s TFP is 1 percent above normal, \(x_t = 1\). A common and simple description of a temporary shock is that each quarter it keeps a fixed fraction \(\rho\) of its size, with \(0 \le \rho < 1\), plus any new shock \(\varepsilon_{t+1}\) that arrives:

\[x_{t+1} = \rho\,x_t + \varepsilon_{t+1}.\]

This is a difference equation (Appendix C.10). Let a single shock hit in quarter \(t\), with no new shocks later (\(\varepsilon = 0\) from then on). Applying the equation quarter after quarter gives

\[x_{t+1} = \rho x_t, \qquad x_{t+2} = \rho x_{t+1} = \rho^2 x_t, \qquad x_{t+3} = \rho^3 x_t, \qquad\dots\]

so the shock shrinks by the factor \(\rho\) every quarter, and after \(h\) quarters only \(\rho^h\) of it is left.

The half-life. After how many quarters \(h\) is half of the shock gone? Set \(\rho^h = 1/2\) and take logs (Appendix C.4):

\[h\ln\rho = \ln\tfrac{1}{2} \quad\Longrightarrow\quad h = \frac{\ln(1/2)}{\ln\rho} = \frac{\ln 2}{-\ln\rho}.\]

Turned around, a shock whose half-life is \(h\) quarters has \(\rho = (1/2)^{1/h}\).

ImportantKey result: the impulse response of a fading shock

\[x_{t+h} = \rho^h\,x_t \tag{1}\]

The sequence \(x_t, x_{t+1}, x_{t+2}, \dots\) is the impulse response function: the path of the shock after one push. The half-life is \(h = \ln 2/(-\ln\rho)\), so the nearer \(\rho\) is to 1, the longer the shock lasts.

Islandia in numbers. Give Islandia a TFP shock as persistent as the one estimated in the Smets–Wouters model, which loses half its size in 16 quarters (4 years). Then \(\rho = 0.5^{1/16} \approx 0.958\): from one quarter to the next the shock keeps about 96 percent of its size.

TipIn words

Suppose a bad season makes every Islandian crew 1 percent less productive. Equation (1) says the damage does not vanish at once: each quarter a fixed share of it carries over. With \(\rho\) near 1, Islandia feels the season for years; with \(\rho\) near 0, it is forgotten within a few quarters.

Why responses can peak late: adjustment costs. Under equation (1), the effect is largest right away, in the quarter of the shock. Yet estimated models find that GDP responds most to a monetary shock 3 to 4 quarters after it. Adjustment costs create this delay. Here is a simple illustration (it is not the Smets–Wouters model itself). A variable \(y_t\) moves only a fraction \(\lambda\) of the way toward its target \(x_t\) each quarter:

\[y_t = y_{t-1} + \lambda\,(x_t - y_{t-1}), \qquad 0 < \lambda \le 1.\]

With \(\lambda = 1\) there are no adjustment costs: \(y_t = x_t\), and \(y\) is largest in the quarter of the shock. With \(\lambda < 1\), \(y\) keeps climbing while the target is still above it, \(x_t > y_{t-1}\), and it turns down only once the fading target drops below it. Think of Harbor Boatworks, which cannot add a whole new shed in one quarter: it builds a bit each quarter, and its capacity is highest some quarters after the good news, not at once. With \(\rho = 0.8\) and \(\lambda = 0.3\), the peak comes 3 quarters after the shock; the check in Model in code confirms it.

A shock fades by a constant fraction each quarter; adjustment costs push the largest effect a few quarters later.

NoteCheck your understanding

1. A shock to Islandia has \(\rho = 0.8\). How much of it is left after one year (4 quarters), and what is its half-life?

Answer

By equation (1), \(0.8^4 \approx 0.41\), so about 41 percent of the shock is left after a year. The half-life is \(h = \ln 2/(-\ln 0.8) \approx 3.1\) quarters.

2. Without adjustment costs, in which quarter is the response largest? Why does \(\lambda < 1\) change that?

Answer

With \(\lambda = 1\), \(y_t = x_t\), and by equation (1) \(x\) is largest in the quarter of the shock. With \(\lambda < 1\), \(y\) closes only part of the gap each quarter, so it keeps rising while the target is still above it, and it peaks later.

What the model predicts

  • DSGE models are quantitative. What they produce is a set of impulse responses: how much, and for how long, GDP, consumption, hours and inflation move after each kind of shock. Their parameters, \(\rho\) among them, are estimated to fit U.S. data.
  • Persistence matters. By equation (1), a shock with \(\rho = 0.958\) keeps 84 percent of its size after one year (\(0.958^4 \approx 0.84\)), while one with \(\rho = 0.5\) keeps only 6 percent (\(0.5^4 \approx 0.06\)).
  • Lags. With adjustment costs, a shock does most of its work after a delay: the “long and variable lags” of monetary policy described by Milton Friedman.
  • Frictions matter. The same shock can have different effects under different frictions. The labor-market blocks below show this.

DSGE models answer in numbers: how far each variable moves after each shock, and for how many quarters.

Real business cycles: the Solow model with TFP shocks

Assumptions

The first DSGE models were the real business cycle (RBC) models of Finn Kydland and Edward Prescott, and of John Long and Charles Plosser. They took the Solow model of Chapter 5 and changed a single thing: total factor productivity stops being a constant \(\bar{A}\) and becomes a variable \(A_t\) that shocks push around.

  • Production. Islandia combines capital (its tools) and labor to make output:

    \[Y_t = A_t K_t^{1/3} L_t^{2/3}. \tag{2}\]

  • TFP shocks. TFP moves around its normal level \(\bar{A}\). Its percentage gap \(x_t\) follows the shock process of the first block, so a TFP shock dies out as equation (1) says.

  • Capital accumulation works as in the Solow model (Chapter 5): \(\Delta K_{t+1} = \bar{s}Y_t - \bar{d}K_t\). Capital is a stock. Within a quarter Islandia’s tools are what they are; only investment changes them.

  • Choices. In a complete RBC model households choose the saving rate and their hours, and every wage and price is flexible.

These shocks are real: they hit technology, not money. They are enough to produce booms and slumps that look like business cycles, which gives the models their name. A fall in TFP does not require Islandia to forget how to build boats. TFP measures how well the inputs are used, so a new tax, a clumsy regulation or a trade disruption can lower it as well (the sales tax of the labor-market block is an example).

Solving the model

Step 1: output growth in terms of its sources. Take logs of equation (2), with \(\ln(ab) = \ln a + \ln b\) and \(\ln a^b = b\ln a\) (Appendix C.4):

\[\ln Y_t = \ln A_t + \tfrac{1}{3}\ln K_t + \tfrac{2}{3}\ln L_t.\]

Write it again for year \(t-1\) and subtract. A change in the log of a variable is close to its growth rate (Appendix C.5):

\[g_Y = g_A + \tfrac{1}{3}g_K + \tfrac{2}{3}g_L.\]

So Islandia’s output grows for three reasons: TFP grows; capital grows, with weight \(1/3\) (capital’s share of income); and labor grows, with weight \(2/3\) (labor’s share).

Step 2: the impact of a TFP shock. Within the quarter the tools are fixed, so \(g_K = 0\) on impact. In the plain Solow model labor is fixed too, \(g_L = 0\), and the formula gives \(g_Y = g_A\): TFP 1 percent higher means output 1 percent higher, at once. Once islanders choose their hours, a TFP shock pushes employment the same way, and the labor-market block below shows that output then rises by about \(3/2\) percent.

Step 3: propagation through capital. More output means more investment, \(\bar{s}Y_t\), so Islandia starts the next quarter with more tools, which raise output again. A persistent TFP shock therefore lifts GDP for longer than the shock itself lasts, and at its peak by more. It is the same accumulation channel that drives transition dynamics in the Solow model.

TipIn words

A good season on Islandia works twice. First it raises what each crew catches with the same boats and nets. Then the extra catch is partly turned into new boats, so the following quarters start with more tools. The shock fades, but the tools it paid for stay a while.

Step 4: measuring TFP as a Solow residual. Output, capital and labor can be measured; TFP cannot. So we solve the growth equation of step 1 for \(g_A\), moving the capital and labor terms to the left:

\[g_A = g_Y - \tfrac{1}{3}g_K - \tfrac{2}{3}g_L.\]

ImportantKey result: TFP growth as a Solow residual

\[g_A = g_Y - \tfrac{1}{3}g_K - \tfrac{2}{3}g_L \tag{3}\]

TFP growth is the part of output growth that the growth of capital and labor cannot account for: the Solow residual. It is the growth accounting of Chapter 6, done one year at a time.

Islandia in numbers. In a year when Islandia’s output grows by 3 percent, its capital by 3 percent and its hours by 1.5 percent, \(g_A = 3 - \tfrac{1}{3}\times 3 - \tfrac{2}{3}\times 1.5 = 3 - 1 - 1 = 1\) percent.

Step 5: why the residual can mislead in recessions. Equation (3) takes for granted that every measured worker and machine is used equally hard in every year. In a slump that is not true. Harbor Boatworks keeps its crews and its boats but uses them less: shorter shifts, boats tied up at the dock, slower work. Call \(u_t\) an index of this utilization, so the inputs really in use are \(u_tK_t\) and \(u_tL_t\). The production function becomes

\[Y_t = A_t (u_tK_t)^{1/3}(u_tL_t)^{2/3} = A_t\,u_t^{1/3+2/3}\,K_t^{1/3}L_t^{2/3} = A_t\,u_t\,K_t^{1/3}L_t^{2/3}.\]

Taking logs and growth rates as in step 1 gives \(g_Y = g_A + g_u + \tfrac{1}{3}g_K + \tfrac{2}{3}g_L\). The residual computed from measured inputs therefore picks up utilization too.

ImportantKey result: what the raw residual measures

\[\underbrace{g_Y - \tfrac{1}{3}g_K - \tfrac{2}{3}g_L}_{\text{raw Solow residual}} = \underbrace{g_A}_{\text{true TFP growth}} + \underbrace{g_u}_{\text{utilization growth}} \tag{4}\]

When utilization drops in a recession, \(g_u < 0\), the raw residual drops with it, even if true TFP has not changed at all. John Fernald estimates \(g_u\) from hours per worker and other indicators and removes it; the result is his utilization-adjusted TFP series.

TFP is measured as a residual, and in a slump that residual also catches idle workers and machines.

NoteCheck your understanding

1. In a bad year Islandia’s output falls by 1 percent, its capital grows by 3 percent and its hours fall by 3 percent. What is the raw Solow residual? Utilization fell by 1.5 percent that year: what was true TFP growth?

Answer

By equation (3), the raw residual is \(-1 - \tfrac{1}{3}\times 3 - \tfrac{2}{3}\times(-3) = -1 - 1 + 2 = 0\) percent. By equation (4), true TFP growth is the raw residual minus utilization growth: \(0 - (-1.5) = 1.5\) percent. TFP rose; this slump was not caused by a fall in TFP.

2. Why does a persistent TFP shock raise output by more, at its peak, than the shock itself?

Answer

Because of capital accumulation (step 3). Higher output brings more investment, \(\bar{s}Y_t\), so capital rises in the following quarters and adds to output on top of the direct effect of TFP in equation (2).

What the model predicts

  • Booms and recessions follow TFP. A rise in TFP lifts output, investment and (when hours are a choice) employment and wages; a fall lowers them. A persistent shock keeps output high for years, and the peak exceeds the shock because capital builds up.
  • The test. If TFP shocks drive the cycle, measured TFP growth should be weak or negative in recession years. Fig. 1 and the two charts after it test this with Fernald’s data and the Penn World Table.
  • The answer depends on the measure (equation 4). The raw Solow residual falls in most recessions, and that was the first evidence for RBC models. But part of the fall is utilization. In 2009, for example, the raw residual was −0.15 percent while utilization fell by 3.68 percent, so true TFP growth was about \(-0.15 - (-3.68) = +3.53\) percent. Without utilization, TFP rises in some recessions, so TFP shocks cannot explain everything.
  • What survived. The RBC method lived on: a growth model with optimizing agents, shocks and numerical solutions. Once other shocks and frictions such as sticky prices were added, RBC models became one special case of today’s DSGE models.

TFP shocks alone cannot explain recessions, but the RBC method became the backbone of DSGE models.

The labor market of a DSGE model

Assumptions

Now we solve the most useful block of a DSGE model with pencil and paper. We look at one quarter, so Islandia’s capital is fixed at \(K\) and TFP sits at \(\bar{A}\).

1. Production. Harbor Boatworks uses the production function of equation (2), without time subscripts: \(Y = \bar{A}K^{1/3}L^{2/3}\).

2. The firm hires until the marginal product of labor equals the real wage. The marginal product of labor (MPL) is the extra output that one more unit of labor brings. It is the derivative of output with respect to \(L\), with \(\bar{A}\) and \(K\) held fixed (Appendix C.7). The power rule brings the exponent \(2/3\) down in front and lowers it by one, to \(2/3 - 1 = -1/3\):

\[\text{MPL} = \frac{\partial Y}{\partial L} = \frac{2}{3}\bar{A}K^{1/3}L^{-1/3} = \frac{2}{3}\cdot\frac{\bar{A}K^{1/3}}{L^{1/3}}.\]

The Islandia Treasury may also tax sales revenue at the rate \(\tau\) (a sales tax or value-added tax). Then Harbor Boatworks keeps only the share \(1-\tau\) of what an extra worker makes. It stops hiring where this after-tax MPL equals the real wage \(w\). That condition is the labor demand curve, \(w = (1-\tau)\cdot\tfrac{2}{3}\cdot\bar{A}K^{1/3}/L^{1/3}\). Without the tax, \(\tau = 0\). The curve slopes down because \(L\) is in the denominator: with diminishing returns, each extra worker adds less.

3. Households supply labor. The islanders’ labor supply curve is \(L^s = \bar{\ell}\,w/c\), where \(c\) is consumption per person and \(\bar{\ell}\) sets the general level of labor supplied (when \(w = c\), \(L^s = \bar{\ell}\)). Islanders work more when the wage is high compared with what they consume. Over long periods wages and consumption grow at about the same pace, so \(w/c\) and hours per person stay roughly flat, just as in the data.

Where the labor supply curve comes from. An islander picks consumption \(c\) and hours \(L\) to maximize utility

\[U(c, L) = \ln c - \frac{1}{2}\cdot\frac{1}{\bar{\ell}}\cdot L^2 \qquad\text{subject to}\qquad c + \bar{S} = wL,\]

where \(\bar{S}\) is a given amount of saving. Utility goes up with consumption and down with hours, and because of the square, each extra hour hurts more than the previous one. Put the budget, \(c = wL - \bar{S}\), into utility, so that it depends on \(L\) only, and set the derivative to zero (Appendix C.8). By the chain rule, the derivative of \(\ln(wL - \bar{S})\) is \(w/(wL-\bar{S}) = w/c\):

\[\frac{dU}{dL} = \frac{w}{c} - \frac{L}{\bar{\ell}} = 0 \quad\Longrightarrow\quad L = \bar{\ell}\cdot\frac{w}{c}.\]

The first term is what one more hour brings: the wage \(w\), valued at the marginal utility of consumption, \(1/c\). The second term is what it costs in lost free time. An islander who already consumes more, with a higher \(c\), values extra income less, and so works less at any wage.

ImportantKey result: the two curves of the labor market

\[w = (1-\tau)\cdot\frac{2}{3}\cdot\frac{\bar{A}K^{1/3}}{L^{1/3}} \tag{5}\]

\[L^s = \bar{\ell}\cdot\frac{w}{c} \tag{6}\]

Equation (5) is Harbor Boatworks’ labor demand: the after-tax marginal product of labor. Equation (6) is the islanders’ labor supply, the best choice of hours for a household that weighs extra income against lost leisure.

4. Consumption is taken as given. In a full DSGE model, \(c\) is chosen with an eye on the whole future. Here we hold it fixed. The permanent-income hypothesis (Chapter 16) supports this shortcut: consumption follows lifetime income, so short-lived shocks hardly move it. Shocks that do change lifetime income, such as a lasting rise in government purchases or news about the future, reach the labor market through \(c\). Consumption is the forward-looking variable of this block.

Harbor Boatworks hires up to the after-tax marginal product; islanders work more when the wage is high relative to their consumption.

Solving the model

Step 1: write labor supply as a wage. Multiply equation (6) by \(c/\bar{\ell}\): \(w = cL/\bar{\ell}\).

Step 2: set the two wages equal. In equilibrium the wage on the supply curve is the wage on the demand curve:

\[\frac{cL}{\bar{\ell}} = (1-\tau)\cdot\frac{2}{3}\cdot\frac{\bar{A}K^{1/3}}{L^{1/3}}.\]

Step 3: collect the powers of \(L\). Multiply both sides by \(L^{1/3}\) and by \(\bar{\ell}/c\). On the left, \(L \cdot L^{1/3} = L^{1 + 1/3} = L^{4/3}\) (Appendix C.3):

\[L^{4/3} = (1-\tau)\cdot\frac{2}{3}\cdot\frac{\bar{A}K^{1/3}\,\bar{\ell}}{c}.\]

Step 4: solve for \(L\). Raise both sides to the power \(3/4\), because \((L^{4/3})^{3/4} = L\). Then put \(L^*\) into the supply curve to get the wage, and into the production function to get output, \(Y^* = \bar{A}K^{1/3}(L^*)^{2/3}\).

Step 5: the baseline. Jones draws his diagrams without numbers. Every diagram of this notebook starts instead from one illustrative set of values: \(\bar{A} = 1\), \(K = 1\), \(\bar{\ell} = 0.9\), \(c = 0.6\) and no tax. Then the bracket below is \(\tfrac{2}{3}\times 0.9/0.6 = 1\).

ImportantKey result: equilibrium employment and the real wage

\[L^* = \left[(1-\tau)\cdot\frac{2}{3}\cdot\frac{\bar{A}K^{1/3}\,\bar{\ell}}{c}\right]^{3/4}, \qquad w^* = \frac{c\,L^*}{\bar{\ell}} \tag{7}\]

Employment rises with TFP, capital, the after-tax share \(1-\tau\) and the willingness to work \(\bar{\ell}\), and falls with consumption \(c\).

Islandia in numbers. In the baseline, \(L^* = 1\), \(w^* = 0.6/0.9 = 2/3\) and \(Y^* = 1\). Islanders consume 60 percent of output, near the U.S. share of GDP, and labor earns \(w^*L^*/Y^* = 2/3\) of output.

The labor share is two thirds for a general reason, not by luck of the numbers: along the labor demand curve with no tax, \(w = \tfrac{2}{3}Y/L\), so labor always takes two thirds of output, as in Chapter 4.

TipIn words

Harbor Boatworks keeps hiring while an extra islander adds more than the wage; islanders keep offering hours while the wage, measured against what they already consume, beats the lost free time. Employment settles where both stop. Anything that makes workers more productive pulls it up; anything that makes islanders feel richer pulls it down.

Step 6: elasticities. Take logs of equation (7). The bracket is a product of powers, so each factor gives its own term:

\[\ln L^* = \text{const} + \tfrac{3}{4}\ln(1-\tau) + \tfrac{3}{4}\ln\bar{A} + \tfrac{1}{4}\ln K + \tfrac{3}{4}\ln\bar{\ell} - \tfrac{3}{4}\ln c.\]

For the wage, \(\ln w^* = \ln c + \ln L^* - \ln\bar{\ell}\), so

\[\ln w^* = \text{const} + \tfrac{3}{4}\ln(1-\tau) + \tfrac{3}{4}\ln\bar{A} + \tfrac{1}{4}\ln K - \tfrac{1}{4}\ln\bar{\ell} + \tfrac{1}{4}\ln c.\]

For output, \(\ln Y^* = \ln\bar{A} + \tfrac{1}{3}\ln K + \tfrac{2}{3}\ln L^*\), so the elasticity of output with respect to TFP is \(1 + \tfrac{2}{3}\cdot\tfrac{3}{4} = \tfrac{3}{2}\). TFP raises output directly, and once more by drawing more islanders into work. This is the \(3/2\) promised in the RBC block.

How much does labor demand react to the wage? Solve equation (5) for \(L\): \(L^{1/3} = (1-\tau)\tfrac{2}{3}\bar{A}K^{1/3}/w\), then cube both sides, \(L^d = \left[(1-\tau)\tfrac{2}{3}\bar{A}K^{1/3}/w\right]^3\). The wage elasticity of labor demand is \(-3\): a wage 1 percent lower makes firms want about 3 percent more labor. Because the demand curve is this flat, shifts in labor supply move the wage only a little.

Equilibrium employment rises with productivity and falls with consumption; a TFP rise lifts output by about one and a half times as much.

NoteCheck your understanding

1. Islandia’s TFP rises by 2 percent for one quarter, with consumption unchanged. By about how much do employment and output rise?

Answer

By the logs of equation (7), the elasticity of \(L^*\) with respect to \(\bar{A}\) is \(3/4\), so employment rises by about \(1.5\) percent. The elasticity of output is \(3/2\), so output rises by about 3 percent: 2 percent directly, and 1 percent more from the extra hours (\(\tfrac{2}{3}\times 1.5\)).

2. Why do islanders who consume more work fewer hours at the same wage?

Answer

In equation (6), hours depend on \(w/c\). The gain from an extra hour is the wage valued at the marginal utility of consumption, \(1/c\). With higher \(c\), an extra unit of income is worth less to them, so the lost free time wins sooner.

What the model predicts

Comparative statics. The coefficients of the logs above are elasticities: the percentage change in \(L^*\) or \(w^*\) when a variable goes up by 1 percent.

A 1% rise in … changes \(L^*\) by changes \(w^*\) by Which curve moves Example of the shock
TFP \(\bar{A}\) \(+0.75\%\) \(+0.75\%\) labor demand shifts up a temporary TFP shock (Fig. 3)
after-tax share \(1-\tau\) \(+0.75\%\) \(+0.75\%\) labor demand shifts up a cut in the sales tax (Fig. 4, reversed)
capital \(K\) \(+0.25\%\) \(+0.25\%\) labor demand shifts up (fixed in the short run)
consumption \(c\) \(-0.75\%\) \(+0.25\%\) labor supply shifts up and left good news (Fig. 9); less government (Fig. 5, reversed)
labor-supply scale \(\bar{\ell}\) \(+0.75\%\) \(-0.25\%\) labor supply shifts down and right more willingness to work (Fig. 2)

The table splits into two halves:

  • Shifts of labor demand move employment and the wage the same way. A fall in TFP, or a new sales tax, lowers both, which looks like a recession. If Islandia’s TFP drops to \(\bar{A}' = 0.8\), equation (7) gives \(L^* = 0.8^{3/4} \approx 0.85\) and \(w^* = 0.6 \times 0.85/0.9 \approx 0.56\). A 20 percent sales tax from the Treasury leads to exactly the same point, because equation (7) contains only the product \((1-\tau)\bar{A}\). That is why early DSGE models placed TFP shocks at the center.
  • Shifts of labor supply move them in opposite directions. Suppose the Islandia Treasury permanently raises its purchases by \(0.1\). Islanders must pay for them with taxes, so their lifetime income and consumption fall by \(0.1\), from 0.6 to 0.5. Employment climbs to \((0.6/0.5)^{3/4} \approx 1.15\), but the wage drops to \(0.5 \times 1.15/0.9 \approx 0.64\). Good news that raises consumption to 0.7 cuts employment to \((0.6/0.7)^{3/4} \approx 0.89\) and lifts the wage to \(0.7 \times 0.89/0.9 \approx 0.69\). Booms with falling wages and falling consumption are not what typical business cycles look like, so these shocks matter, but they are not the main driver.

A permanent TFP rise moves both curves: labor demand rises through \(\bar{A}\), and labor supply shifts up through a higher \(c\). Its effect on employment is ambiguous, because \(\bar{A}\) and \(c\) enter equation (7) only as the ratio \(\bar{A}/c\). If both rise by 10 percent, the ratio is unchanged, employment is unchanged, and the wage \(w^* = cL^*/\bar{\ell}\) rises by the full 10 percent.

Demand shifts move employment and wages together; supply shifts move them apart. Real cycles look like the first kind.

NoteCheck your understanding

1. The Islandia Treasury permanently raises its purchases by \(0.05\), so consumption falls from 0.6 to 0.55. What are the new employment and real wage?

Answer

By equation (7), only \(c\) changes, so \(L^* = (0.6/0.55)^{3/4} \approx 1.067\), up by about 6.7 percent. The wage is \(w^* = 0.55 \times 1.067/0.9 \approx 0.652\), below the baseline \(2/3\). Employment rises while the wage falls: a labor-supply shift.

2. Why does a 20 percent sales tax have exactly the same effect on employment as a 20 percent fall in TFP?

Answer

In equation (7), \(\tau\) and \(\bar{A}\) appear only through the product \((1-\tau)\bar{A}\). Both shocks cut it from 1 to 0.8, so they shift the labor demand curve of equation (5) by the same amount and lead to the same \(L^*\) and \(w^*\).

New Keynesian additions: sticky wages and sticky prices

Assumptions

When wages and prices are flexible, the classical dichotomy holds: money moves only nominal variables, and the Bank of Islandia cannot change employment. New Keynesian models add a nominal rigidity. To see it we need nominal variables. Let \(W\) be the nominal wage (money per hour) and \(P\) the price level, so the real wage is

\[w = \frac{W}{P}.\]

We add the frictions one by one.

  • Sticky wages. The nominal wage is stuck at \(W = \bar{W}\), while other prices can move. A monetary expansion (the Bank of Islandia cuts its policy rate) raises spending and lifts the price level from \(P_0\) to a higher \(P'\).
  • Trade is voluntary. If the wage does not clear the market, the amount traded is the smaller of the amounts demanded and supplied: the short side of the market decides.
  • Sticky prices. Now the wage is flexible, but firms adjust their prices only slowly. At its posted price, Harbor Boatworks makes whatever its customers order, so total spending sets output \(Y\). A monetary expansion raises the output demanded.

A stuck wage or a stuck price is what gives the Bank of Islandia power over real activity.

Solving the model

Sticky wages. The real wage is now \(\bar{W}/P\): the nominal wage and the price level set it, not the market. Harbor Boatworks demands \(L^d(\bar{W}/P)\), from labor demand solved for \(L\) in the previous block, and islanders offer \(L^s(\bar{W}/P) = \bar{\ell}\,(\bar{W}/P)/c\), from equation (6). The short side decides how much work is done.

ImportantKey result: employment with a sticky wage

\[L = \min\left\{L^d\!\left(\frac{\bar{W}}{P}\right),\ L^s\!\left(\frac{\bar{W}}{P}\right)\right\} \tag{8}\]

When the stuck real wage is above the market-clearing \(w^*\), the firm is the short side. The gap \(L^s - L^d\) is involuntary unemployment: islanders who would work at the going wage but find no job.

Islandia in numbers (the values of Figs. 6–7). With \(\bar{W} = 0.75\) and \(P_0 = 1\), the real wage is 0.75, above \(w^* = 2/3\). The firm hires \(L^d = \left(\tfrac{2}{3}/0.75\right)^3 = (8/9)^3 \approx 0.70\), islanders offer \(0.9 \times 0.75/0.6 = 1.125\), and unemployment is about \(0.42\). If prices rise by 5 percent, to \(P' = 1.05\), the real wage falls to \(0.75/1.05 \approx 0.71\), employment grows by the factor \(1.05^3 \approx 1.16\) to about \(0.81\), and unemployment shrinks to about \(0.26\).

A monetary expansion. A higher price level lowers the real wage \(\bar{W}/P\), so Harbor Boatworks hires more. In logs, along labor demand, \(\ln L^d = \text{const} - 3\ln\bar{W} + 3\ln P\): employment has an elasticity of \(+3\) with respect to the price level, while the real wage falls one for one with \(P\). In time the nominal wage catches up, \(W' = w^*P'\), the real wage is back at \(w^*\) and the market clears: in the long run money is neutral once more.

Sticky prices. Output is set by demand, and the firm hires just the labor it needs to produce it. Invert the production function, \(L^{2/3} = Y/(\bar{A}K^{1/3})\), and raise both sides to the power \(3/2\).

ImportantKey result: employment with sticky prices

\[L = \left(\frac{Y}{\bar{A}K^{1/3}}\right)^{3/2} \tag{9}\]

The wage does not appear, so labor demand is a vertical line in the labor-market diagram. The nominal wage is flexible and adjusts until islanders want to supply exactly this \(L\), so the real wage lies on the supply curve, \(W/P = cL/\bar{\ell}\).

Islandia in numbers (the values of Fig. 8). Spending on Islandia’s output rises by 15 percent, from \(Y_0 = 1\) to \(Y' = 1.15\). Employment rises to \(1.15^{3/2} \approx 1.23\), and the real wage to \(0.6 \times 1.23/0.9 \approx 0.82\).

Elasticities. Taking logs of equation (9) and of the supply curve: for each 1 percent more output demanded, employment rises by \(3/2\) percent, and so does the real wage (with \(c\) fixed). A rise in TFP with unchanged demand lowers employment, with elasticity \(-3/2\): Harbor Boatworks needs fewer workers to fill the same orders.

TipIn words

With a sticky wage, a higher price level makes each hour cheaper for Harbor Boatworks, so it hires more, but the islanders’ real pay falls. With sticky prices, the Bank of Islandia raises spending, the firm must fill more orders, and it bids for more hours, so employment and the real wage rise together.

With sticky wages, money raises employment by cutting the real wage; with sticky prices, it raises both.

NoteCheck your understanding

1. The nominal wage stays at \(\bar{W} = 0.75\), but the Bank of Islandia’s expansion lifts the price level by 10 percent, to \(P = 1.1\). What are employment and unemployment now?

Answer

The real wage is \(0.75/1.1 \approx 0.68\), still above \(w^* = 2/3\), so the firm is the short side in equation (8). Employment rises by the factor \(1.1^3 = 1.331\), from about 0.70 to about \(0.935\). Islanders offer \(0.9 \times 0.68/0.6 \approx 1.023\), so unemployment falls to about \(0.088\).

2. With sticky prices and output demanded fixed at \(Y = 1\), Islandia’s TFP rises by 10 percent. What happens to employment?

Answer

By equation (9), \(L = (1/1.1)^{3/2} \approx 0.867\): employment falls by about 13 percent. The firm makes the same output with fewer hours, because demand, not the wage, sets output.

What the model predicts

Which way do employment and the real wage move? In actual business cycles, employment and the real wage tend to rise together in booms. The table collects what the blocks say:

Shock Prices and wages What moves in the diagram Employment Real wage Looks like a business cycle?
TFP falls flexible labor demand shifts down falls falls yes
sales tax rises flexible labor demand shifts down falls falls yes
government purchases rise flexible labor supply shifts right (\(c\) falls) rises falls no
monetary expansion sticky wages down along labor demand (\(\bar{W}/P\) falls) rises falls no
monetary expansion sticky prices vertical labor demand shifts right rises rises yes
TFP rises, demand unchanged sticky prices vertical labor demand shifts left falls falls no
  • Two flavors of DSGE models. The shocks that move employment and wages together are TFP shocks with flexible prices (the RBC flavor) and monetary shocks with sticky prices (the new Keynesian flavor). Modern DSGE models mix the two, with many more shocks and frictions.
  • Sticky wages alone fall short. They let money matter and they create unemployment, but they make the real wage countercyclical: it falls as employment rises.
  • Frictions can flip a sign. With sticky prices, higher TFP can reduce hours at first (equation 9), as the estimated Smets–Wouters model finds. Good news about the future lowers employment with flexible prices but raises it with sticky prices (Figs. 9–10). How sticky prices really are is thus one of the big open questions of macroeconomics.
  • Money is neutral in the long run. Once nominal wages and prices adjust, the economy returns to the flexible equilibrium of equation (7).

The same shock can raise or lower employment: the frictions decide, which is why DSGE models need both flavors.

NoteRecap
  • A DSGE model is built from optimizing agents, shocks and frictions; a shock fades as \(x_{t+h} = \rho^h x_t\), and adjustment costs delay the peak.
  • Real business cycles: TFP shocks move output, and TFP is measured as a Solow residual, which also catches utilization in a slump (Fig. 1).
  • The labor market clears at \(L^*\) and \(w^*\) of equation (7); in the baseline \(L^* = 1\) and \(w^* = 2/3\) (Fig. 2).
  • Labor-demand shocks (TFP, a sales tax) move employment and the wage together, like a real recession (Figs. 3–4); labor-supply shocks move them apart (Fig. 5).
  • Sticky wages let money raise employment but make the real wage fall (Fig. 7); sticky prices make both rise (Fig. 8).
  • Frictions can flip signs: good news lowers employment with flexible prices and raises it with sticky prices (Figs. 9–10).

Model in code

Now we turn the Key results into Python. The section has four parts: the setup (folded, because it is the same in every chapter), the model as functions, numerical checks of the Key results, and the drawing tools that the labor-market diagrams use (folded too). Run its cells once, in order. Every later section of the notebook reuses them.

Setup

Run the setup cells once, before anything else. They load the Python libraries, give every chart the same look, store the address of the chapter’s data and define a helper that only the book website needs. You can use the notebook without reading them; open the box if you are curious.

The first cell loads the libraries and sets the look of the charts. Its comments explain each step.

# @title Setup: libraries, chart colors and the data location
# Text after a "#" is a comment: Python ignores it, it is there for human readers.

# Step 1: load the libraries. `import` loads a library, a toolbox written by
# other people, and `as np` gives it a short nickname, so the code stays short.
import numpy as np                    # numbers and arrays
import pandas as pd                   # data tables
import plotly.express as px           # quick charts from data tables
import plotly.graph_objects as go     # charts built piece by piece (model figures)
import plotly.io as pio               # settings shared by every chart
# sliders and buttons for the interactive figures
from ipywidgets import interact, fixed, FloatSlider, IntSlider, ToggleButtons, Dropdown

# Step 2: one light blue/green look for every chart
BLUE, TEAL, GREEN, GRAY = "#16528c", "#008070", "#7fb23a", "#8a96a3"
pio.templates.default = "plotly_white"
# the same chart size in Colab and on the website
pio.templates["plotly_white"].layout.update(width=860, height=480)
px.defaults.color_discrete_sequence = [BLUE, TEAL, GREEN, "#5aa0d8", "#0b4f47"]
WIDE = {"description_width": "initial"}         # show the full slider labels

# Step 3, Colab only: let Plotly charts appear inside the interactive widgets.
# `try` runs the indented lines; if the import fails (we are not in Colab),
# `except` skips them.
try:
    import google.colab
    google.colab.output.enable_custom_widget_manager()
except ImportError:
    pass                                        # not running in Colab: nothing to do

# Step 4: the data files of this chapter live in the book's GitHub repository
DATA_URL = "https://raw.githubusercontent.com/quarcs-lab/macro102-site/main/data/ch15/"

The website cannot run Python, so each interactive figure also gets a version with a precomputed slider. The next helper builds those versions. Run it and move on.

# @title Helper for the website versions (run this cell; you do not need to read it)
def slider_figure(make_figure, name, values, start, label=None):
    """Join precomputed versions of a figure with one slider (the website cannot run Python).

    Arguments
        make_figure : the function that draws the figure, e.g. labor_market
        name        : the argument the slider moves, e.g. "c"
        values      : the values of that argument to precompute
        start       : the value shown first
        label       : the slider's label (the argument name if None)
    Returns
        one Plotly figure with all versions and a slider that shows one at a time
    """
    # Step 1: draw the figure once for every value
    figures = [make_figure(**{name: v}) for v in values]
    first = values.index(start)
    # Step 2: copy every curve into one figure; only the start value's are visible.
    # `go.Figure` starts a chart, and `add_trace` adds one line or set of points to it.
    fig = go.Figure(layout=figures[first].layout)
    owner = []                          # which precomputed figure each trace is from
    for i, f in enumerate(figures):
        for trace in f.data:
            trace.visible = (i == first)
            fig.add_trace(trace)
            owner.append(i)
    # Step 3: one slider step per value: its curves, title, labels and shapes
    steps = []
    for i, f in enumerate(figures):
        layout = {"title": f.layout.title.to_plotly_json(),
                  "annotations": [a.to_plotly_json() for a in f.layout.annotations],
                  "shapes": [s.to_plotly_json() for s in f.layout.shapes]}
        visible = [o == i for o in owner]
        steps.append(dict(method="update", label=str(values[i]), args=[{"visible": visible}, layout]))
    fig.update_layout(sliders=[dict(active=first, steps=steps, pad=dict(t=60),
                                    currentvalue=dict(prefix=(label or name) + " = "))])
    return fig

The model as functions

The labor market of a DSGE model fits in a few Key results: labor demand, equation (5), labor supply, equation (6), and the market-clearing point, equation (7). We write each one as a Python function: a named recipe that takes inputs and gives back a result. The first cell sets Islandia’s baseline and writes the two curves twice, once as a wage for a given \(L\) (for drawing) and once as hours for a given \(w\) (for the checks).

# The baseline: TFP A-bar, capital K, labor-supply scale ell-bar and consumption c
A_BAR, K_BAR, ELL_BAR, C_BAR = 1.0, 1.0, 0.9, 0.6


# `def` creates a function. Its inputs are the arguments in parentheses.
# `A=A_BAR` gives A a default value, used whenever we do not pass one.
# The text in triple quotes describes the function.
def demand_wage(L, A=A_BAR, tau=0.0):
    """The labor demand curve, written as a wage.

    Equation (5):  w = (1 - tau) (2/3) A K^(1/3) / L^(1/3)

    Arguments
        L   : employment (hours of work)
        A   : TFP, A-bar
        tau : the sales tax rate (0 unless we pass another value)
    Returns
        the real wage w at which firms want to hire L
    """
    # `return` gives back the result; `**` means "to the power of",
    # so `L ** (1 / 3)` is the cube root of L
    return (1 - tau) * (2 / 3) * A * K_BAR ** (1 / 3) / L ** (1 / 3)


def supply_wage(L, c=C_BAR, ell=ELL_BAR):
    """The labor supply curve, written as a wage.

    Equation (6) solved for w:  w = c L / ell

    Arguments
        L   : employment (hours of work)
        c   : consumption
        ell : the willingness to work, ell-bar
    Returns
        the real wage w at which households want to work L
    """
    # the wage that makes the household's best choice of hours equal to L
    return c * L / ell


def labor_demand(w, A=A_BAR, tau=0.0):
    """Labor demanded at the real wage w.

    Equation (5) solved for L:  L = ((1 - tau) (2/3) A K^(1/3) / w)^3

    Arguments
        w   : the real wage
        A   : TFP, A-bar
        tau : the sales tax rate
    Returns
        the employment L that firms demand
    """
    # the marginal product of labor equals the after-tax wage at this L
    return ((1 - tau) * (2 / 3) * A * K_BAR ** (1 / 3) / w) ** 3


def labor_supply(w, c=C_BAR, ell=ELL_BAR):
    """Labor supplied at the real wage w.

    Equation (6):  L = ell w / c

    Arguments
        w   : the real wage
        c   : consumption
        ell : the willingness to work, ell-bar
    Returns
        the employment L that households supply
    """
    # a higher wage raises hours, higher consumption lowers them
    return ell * w / c

The next cell solves the market: where the two curves cross, equation (7). It also holds the production function, equation (2), the labor that firms need when prices are sticky, equation (9), and TFP growth as a Solow residual, equation (3).

def equilibrium(A=A_BAR, tau=0.0, c=C_BAR, ell=ELL_BAR):
    """The labor market clears: employment L* and the real wage w*.

    Equation (7):  L* = ((1 - tau) (2/3) A K^(1/3) ell / c)^(3/4),  w* = c L* / ell

    Arguments
        A   : TFP, A-bar
        tau : the sales tax rate
        c   : consumption
        ell : the willingness to work, ell-bar
    Returns
        two numbers: employment L* and the real wage w*
    """
    # Step 1: employment where labor demand equals labor supply
    L = ((1 - tau) * (2 / 3) * A * K_BAR ** (1 / 3) * ell / c) ** 0.75
    # Step 2: the wage read off the supply curve. A function can return two
    # results at once, separated by a comma.
    return L, supply_wage(L, c, ell)


def output(L, A=A_BAR):
    """Output, from the production function with capital fixed.

    Equation (2):  Y = A K^(1/3) L^(2/3)

    Arguments
        L : employment
        A : TFP, A-bar
    Returns
        output Y
    """
    # capital K is fixed in the short run, so only A and L move output
    return A * K_BAR ** (1 / 3) * L ** (2 / 3)


def labor_needed(Y, A=A_BAR):
    """Sticky prices: the labor firms need to produce the output demanded.

    Equation (9):  L = (Y / (A K^(1/3)))^(3/2)

    Arguments
        Y : the output demanded
        A : TFP, A-bar
    Returns
        the employment L that produces Y
    """
    # the production function (2) solved for L
    return (Y / (A * K_BAR ** (1 / 3))) ** 1.5


def solow_residual(g_Y, g_K, g_L):
    """TFP growth measured as a Solow residual.

    Equation (3):  g_A = g_Y - (1/3) g_K - (2/3) g_L

    Arguments
        g_Y, g_K, g_L : the growth rates of output, capital and hours (percent)
    Returns
        TFP growth g_A, the part of output growth that capital and hours do not explain
    """
    # output growth minus the contributions of capital and labor
    return g_Y - g_K / 3 - 2 * g_L / 3

Checking the key results

The formulas came from algebra. Now we check them with numbers, one Key result at a time, starting with Islandia’s baseline: the formula of equation (7), labor demand and labor supply must all agree.

# Step 1: the baseline equilibrium, equation (7). `L0, w0 = equilibrium()`
# unpacks the two results into two variables.
L0, w0 = equilibrium()
Y_0 = output(L0)

# Step 2: show the numbers. An f-string, f"... {value:.2f}", puts a value into
# the text; ":.2f" means "two decimals".
print(f"Employment L* = {L0:.2f},  real wage w* = {w0:.3f},  output Y = {Y_0:.2f}")
print(f"Consumption share c/Y = {C_BAR / Y_0:.2f},  labor share wL/Y = {w0 * L0 / Y_0:.3f}")
# Step 3: both curves must pass through (L*, w*)
print(f"Labor demand and labor supply at w*: {labor_demand(w0):.3f} and {labor_supply(w0):.3f}")
print(f"Wage on the demand curve at L*: {demand_wage(L0):.3f},  on the supply curve: {supply_wage(L0):.3f}")
Employment L* = 1.00,  real wage w* = 0.667,  output Y = 1.00
Consumption share c/Y = 0.60,  labor share wL/Y = 0.667
Labor demand and labor supply at w*: 1.000 and 1.000
Wage on the demand curve at L*: 0.667,  on the supply curve: 0.667

Both curves pass through the same point, so the market clears at \(L^* = 1\) and \(w^* = 0.667\).

Check 1: the labor supply curve comes from utility maximization. We let the computer try 200,001 values of hours \(L\) for a household with the wage \(w = 2/3\) and saving \(\bar{S} = 2/3 - 0.6\), compute its utility for each, and pick the best. Equation (6) predicts \(L = 0.9 \times (2/3)/0.6 = 1\) and consumption \(c = 0.6\).

# Step 1: the wage and a given amount of saving
w, S = 2 / 3, 2 / 3 - 0.6
# Step 2: candidate hours of work L. `np.linspace(0.5, 1.5, 200001)` gives
# 200,001 evenly spaced numbers from 0.5 to 1.5; arithmetic on them works on
# all of them at once.
hours = np.linspace(0.5, 1.5, 200001)
cons = w * hours - S                            # the budget: c + S = w L
# Step 3: utility of each choice; `np.log` is the natural logarithm
U = np.log(cons) - 0.5 * hours ** 2 / ELL_BAR
# Step 4: `np.argmax(U)` is the position of the largest utility, and
# `hours[best]` the hours at that position
best = np.argmax(U)
print(f"Best hours L = {hours[best]:.4f},  consumption c = {cons[best]:.4f}")
print(f"Labor supply curve, equation (6): ell w / c = {labor_supply(w, cons[best]):.4f}")
Best hours L = 1.0000,  consumption c = 0.6000
Labor supply curve, equation (6): ell w / c = 1.0000

Check 2: the elasticities. We raise each variable by 1 percent and see by what percentage \(L^*\) and \(w^*\) change. The comparative-statics table predicts \(+0.75\) and \(+0.75\) for \(\bar{A}\), \(-0.75\) and \(+0.25\) for \(c\), and \(+0.75\) and \(-0.25\) for \(\bar{\ell}\). For output, the elasticity with respect to TFP should be \(+1.5\).

# Step 1: one variable 1% higher at a time; percentage changes against the baseline
L1, w1 = equilibrium(A=1.01)
print(f"1% higher A:   L* {100 * (L1 / L0 - 1):+.3f}%,  w* {100 * (w1 / w0 - 1):+.3f}%")
L1, w1 = equilibrium(c=0.6 * 1.01)
print(f"1% higher c:   L* {100 * (L1 / L0 - 1):+.3f}%,  w* {100 * (w1 / w0 - 1):+.3f}%")
L1, w1 = equilibrium(ell=0.9 * 1.01)
print(f"1% higher ell: L* {100 * (L1 / L0 - 1):+.3f}%,  w* {100 * (w1 / w0 - 1):+.3f}%")
# Step 2: output with 1% higher TFP, which also raises L*
L1, w1 = equilibrium(A=1.01)
print(f"1% higher A:   Y* {100 * (output(L1, 1.01) / Y_0 - 1):+.3f}%")
# Step 3: the wage elasticity of labor demand, -3 from equation (5)
print(f"1% lower wage: labor demanded {100 * (labor_demand(w0 * 0.99) / labor_demand(w0) - 1):+.3f}%")
1% higher A:   L* +0.749%,  w* +0.749%
1% higher c:   L* -0.743%,  w* +0.249%
1% higher ell: L* +0.749%,  w* -0.248%
1% higher A:   Y* +1.504%
1% lower wage: labor demanded +3.061%

The answers are close to the predicted elasticities but not exact, because an elasticity describes very small changes and 1 percent is small but not zero.

Check 3: sticky wages and sticky prices. Equation (8) with \(\bar{W} = 0.75\) should give employment of about 0.70 at \(P_0 = 1\) and about 0.81 at \(P' = 1.05\), a rise by the factor \(1.05^3\). Equation (9) with \(Y' = 1.15\) should give employment \(1.15^{3/2}\) and a higher real wage.

# Step 1: sticky wages. The nominal wage W-bar is fixed; `for P in (1.0, 1.05):`
# runs the indented lines once for each price level in the brackets.
W_bar = 0.75
for P in (1.0, 1.05):
    real_wage = W_bar / P
    # equation (8): `min(a, b)` is the smaller of two numbers, the short side
    # of the market
    employed = min(labor_demand(real_wage), labor_supply(real_wage))
    unemployed = labor_supply(real_wage) - labor_demand(real_wage)
    print(f"P = {P:.2f}: real wage {real_wage:.3f}, employment {employed:.3f}, unemployment {unemployed:.3f}")
print(f"Ratio of employment {labor_demand(W_bar / 1.05) / labor_demand(W_bar):.4f}  vs  1.05^3 = {1.05 ** 3:.4f}")
# Step 2: sticky prices, equation (9): firms hire the labor that the demand Y' needs
L_new = labor_needed(1.15)
print(f"Sticky prices, Y' = 1.15: employment {L_new:.3f} (1.15^1.5 = {1.15 ** 1.5:.3f}), "
      f"real wage {supply_wage(L_new):.3f}")
# Step 3: with sticky prices, higher TFP lowers the labor needed for the same output
print(f"Sticky prices, TFP +10% with Y = 1: employment {labor_needed(1.0, A=1.1):.3f}")
P = 1.00: real wage 0.750, employment 0.702, unemployment 0.423
P = 1.05: real wage 0.714, employment 0.813, unemployment 0.258
Ratio of employment 1.1576  vs  1.05^3 = 1.1576
Sticky prices, Y' = 1.15: employment 1.233 (1.15^1.5 = 1.233), real wage 0.822
Sticky prices, TFP +10% with Y = 1: employment 0.867

Check 4: the Solow residual. First the example in the text: output grows by 3 percent, capital by 3 percent and hours by 1.5 percent. Then equation (4) in Fernald’s data: in every year the raw residual should equal adjusted TFP growth plus the growth of utilization.

# Step 1: the example in the text, equation (3)
print(f"Solow residual, equation (3): g_A = {solow_residual(3.0, 3.0, 1.5):.2f} percent")
# Step 2: `pd.read_csv` reads a data file into a DataFrame, a table with named
# columns; `annual.tfp_solow` is one column, and subtracting columns works row by row
annual = pd.read_csv(DATA_URL + "tfp_annual.csv")
# equation (4): raw residual = adjusted TFP growth + utilization growth
gap = annual.tfp_solow - (annual.tfp_util + annual.utilization)
# Step 3: `.abs().max()` is the largest absolute gap over all years
print(f"Largest gap between the two sides of equation (4), {annual.year.min()}-{annual.year.max()}: "
      f"{gap.abs().max():.3f} percentage points")
# Step 4: the rows where the year is 2009, the Great Recession
year_2009 = annual[annual.year == 2009]
print(year_2009[["year", "gdp_growth", "tfp_solow", "utilization", "tfp_util"]].to_string(index=False))
Solow residual, equation (3): g_A = 1.00 percent
Largest gap between the two sides of equation (4), 1950-2025: 0.033 percentage points
 year  gdp_growth  tfp_solow  utilization  tfp_util
 2009     -3.7508    -0.1489      -3.6778    3.5271

The two sides of equation (4) agree to within a few hundredths of a percentage point in every year of the published data.

Check 5: impulse responses. We simulate the shock process \(x_{t+1} = \rho x_t\) one quarter at a time and compare it with equation (1), using the Smets–Wouters half-life of 16 quarters.

# Step 1: the persistence rho that gives a half-life of 16 quarters
rho = 0.5 ** (1 / 16)
x = 1.0                                        # a 1 percent shock in quarter 0
# Step 2: `range(17)` counts 0, 1, ..., 16, so the `for` loop runs 17 times.
# `if quarter in (4, 16)` is true only in those quarters.
for quarter in range(17):
    if quarter in (4, 16):
        print(f"Quarter {quarter:2d}: simulated {x:.4f},  equation (1): rho^h = {rho ** quarter:.4f}")
    # the shock fades: `x = rho * x` replaces the old x by the new one
    x = rho * x
# Step 3: the half-life implied by rho
print(f"rho = {rho:.4f},  half-life ln(2) / (-ln rho) = {np.log(2) / -np.log(rho):.1f} quarters")
Quarter  4: simulated 0.8409,  equation (1): rho^h = 0.8409
Quarter 16: simulated 0.5000,  equation (1): rho^h = 0.5000
rho = 0.9576,  half-life ln(2) / (-ln rho) = 16.0 quarters

Then we add the adjustment-cost rule \(y_t = y_{t-1} + \lambda(x_t - y_{t-1})\) and find the quarter in which \(y\) peaks, first with \(\lambda = 1\) (no adjustment costs), then with \(\lambda = 0.3\).

# Step 1: a fast-fading shock
rho = 0.8
# Step 2: no adjustment costs, then slow adjustment
for lam in (1.0, 0.3):
    x, y = 1.0, 0.0
    peak_quarter, peak = 0, 0.0
    # Step 3: follow y for 20 quarters and remember its highest point
    for quarter in range(20):
        # close the fraction lam of the gap to the target x
        y = y + lam * (x - y)
        if y > peak:
            peak_quarter, peak = quarter, y
        x = rho * x                            # the target fades, equation (1)
    print(f"lam = {lam}: the response peaks in quarter {peak_quarter}, at {peak:.3f}")
lam = 1.0: the response peaks in quarter 0, at 1.000
lam = 0.3: the response peaks in quarter 3, at 0.509

Drawing tools

All the labor-market diagrams below use the same axes and the same few drawing steps, so we write them once: add_curve draws a labeled curve, add_point marks a point, add_arrow draws an arrow and labor_axes fixes the axes. Run them and move on, or open the box to see how they work.

Every curve carries its name on the plot, at its last point inside the axes, so the diagrams need no legend. The first cell fixes the grid and the axes, and add_curve draws one curve and writes its name, unless another name already sits close by.

# @title Drawing tool: a curve with its name on the plot
# Step 1: employment levels at which we draw the curves; because the model
# functions work on a whole array at once, one call gives a whole curve
L_GRID = np.linspace(0.02, 1.8, 179)
# fixed axes, so that you can see the curves move
L_RANGE, W_RANGE = [0, 1.8], [0, 1.2]


def add_curve(fig, L, w, label, color, dash="solid", avoid=None, start=False):
    """Draw a curve inside the axes and write its name at its last point (its first if start).

    The name is left out when that point is close to `avoid`, the end of another curve.

    Arguments
        fig   : the figure to draw on
        L, w  : the points of the curve (arrays of employment and wages)
        label : the curve's name; "L<sup>d</sup>" uses HTML to write a raised d
        color : its color, e.g. BLUE
        dash  : "solid" or "dash"
        avoid : the end point of another curve, or None
        start : True to write the name at the first, top-left point
    Returns
        the point (L, w) where the name goes
    """
    # Step 1: keep the part of the curve inside the axes. `w <= W_RANGE[1]` is
    # True or False for each point; `L[inside]` keeps the points where it is True.
    inside = w <= W_RANGE[1]
    L, w = L[inside], w[inside]
    fig.add_trace(go.Scatter(x=L, y=w, mode="lines", name="", showlegend=False,
                             hovertemplate="L = %{x:.2f}, w = %{y:.2f}<extra></extra>",
                             line=dict(color=color, width=3, dash=dash)))
    # Step 2: [-1] is the last point inside the axes, [0] the first
    end = (L[0], w[0]) if start else (L[-1], w[-1])
    # Step 3: `if` runs the indented lines only when the condition holds: here,
    # write the name only when no other curve ends close by
    if avoid is None or abs(end[0] - avoid[0]) + abs(end[1] - avoid[1]) > 0.1:
        fig.add_annotation(x=end[0], y=end[1], text=label, showarrow=False, yanchor="middle",
                           xanchor="right" if start else "left", font=dict(color=color, size=15))
    return end

add_point marks an equilibrium with a dot, a letter and dotted guides to both axes.

# @title Drawing tool: a point with its letter and guides to the axes
def add_point(fig, L, w, label, guides=True, where="top right"):
    """Mark the point (L, w) with a dot and a letter, and with dotted lines to both axes.

    Arguments
        fig    : the figure to draw on
        L, w   : the point
        label  : its letter, e.g. "A"
        guides : True to draw the dotted lines to the axes
        where  : where the letter goes, e.g. "top right"
    """
    # Step 1: the dotted guides, from the w-axis to the point and down to the L-axis
    if guides:
        fig.add_trace(go.Scatter(x=[0, L, L], y=[w, w, 0], mode="lines", showlegend=False, hoverinfo="skip",
                                 line=dict(color="black", width=1, dash="dot")))
    # Step 2: the dot and its letter
    fig.add_trace(go.Scatter(x=[L], y=[w], mode="markers+text", text=[label], textposition=where,
                             name="", showlegend=False, textfont=dict(size=16),
                             marker=dict(color="black", size=10),
                             hovertemplate="L = %{x:.2f}, w = %{y:.2f}<extra></extra>"))

add_arrow shows how a point moves, from the old equilibrium to the new one.

# @title Drawing tool: an arrow between two points
def add_arrow(fig, x0, y0, x1, y1):
    """Draw an arrow from (x0, y0) to (x1, y1) in axis units; nothing if the two points almost coincide.

    Arguments
        fig    : the figure to draw on
        x0, y0 : where the arrow starts
        x1, y1 : where it points
    """
    # draw it only if the two points are visibly apart
    if abs(x1 - x0) + abs(y1 - y0) > 0.02:
        fig.add_annotation(x=x1, y=y1, ax=x0, ay=y0, xref="x", yref="y", axref="x", ayref="y", text="",
                           showarrow=True, arrowhead=3, arrowwidth=2, arrowcolor="black")

The axes of every diagram are fixed, so that you can see the curves move. labor_axes sets them and the title.

# @title Drawing tool: the fixed axes of a labor-market diagram
def labor_axes(fig, title, wage="Real wage, w"):
    """Fixed axes for every labor-market diagram (no tick in the bottom-left corner) and the title readout.

    Arguments
        fig   : the figure to finish
        title : the title, e.g. the employment and wage readout
        wage  : the label of the vertical axis
    Returns
        the same figure, with its axes and title set
    """
    # Step 1: employment on the horizontal axis, the wage on the vertical one
    fig.update_xaxes(title_text="Employment, L", range=L_RANGE, tickvals=[0.5, 1.0, 1.5], zeroline=False)
    fig.update_yaxes(title_text=wage, range=W_RANGE, tickvals=[0.2, 0.4, 0.6, 0.8, 1.0], zeroline=False)
    # Step 2: the title, and no legend (the curves carry their names)
    fig.update_layout(title=title, showlegend=False)
    return fig

TFP and real business cycles

DSGE models grew out of real business cycle models, in which swings in total factor productivity drive the cycle. We start by looking at how much U.S. TFP growth moves from year to year.

Fig. 1: U.S. total factor productivity growth

Goal. Look at the shocks that the first DSGE models put at the center of the business cycle.

The first DSGE models, the real business cycle (RBC) models of Finn Kydland and Edward Prescott, took the Solow model of Chapter 5 and let total factor productivity \(\bar{A}\) jump up and down from year to year: these are TFP shocks. A good year for TFP brings a boom, a bad year a recession. Are TFP shocks big enough for that? Growth accounting (Chapter 6) measures TFP growth as the part of output growth that capital and labor do not explain (equation 3). The data are John Fernald’s series for the U.S. business sector (Federal Reserve Bank of San Francisco).

# Load the data of Fig. 1 and look at its first five rows (`.head()`)
tfp = pd.read_csv(DATA_URL + "fig15-1.csv")
tfp.head()
year tfp_growth
0 1950 2.868240
1 1951 0.335407
2 1952 0.885970
3 1953 4.033540
4 1954 3.855820

Each row is one year: tfp_growth is TFP growth in percent. To shade the recessions, we also read the NBER recession dates, from the month before each recession (the peak) to its last month (the trough).

# Step 1: the NBER recession dates; `parse_dates` reads the two columns as dates
recessions = pd.read_csv(DATA_URL + "recessions.csv", parse_dates=["peak", "trough"])


def recession_bands(dates):
    """The recessions as bands of decimal years, from peak to trough (Fig. 1).

    Our years are plain numbers, so each date becomes a decimal year: July 1953
    becomes 1953 + 6/12 = 1953.5. A band runs from the start of the peak month to
    the end of the trough month.

    Arguments
        dates : the table of recession dates, with the columns peak and trough
    Returns
        a list of (start, end) pairs of decimal years, one per recession
    """
    bands = []
    # `zip(...)` walks through the two columns together, one recession at a time
    for peak, trough in zip(dates.peak, dates.trough):
        start = peak.year + (peak.month - 1) / 12     # the start of the peak month
        end = trough.year + trough.month / 12         # the end of the trough month
        bands.append((start, end))
    return bands


# Step 2: the last three recessions
recessions.tail(3)
peak trough
8 2001-03-01 2001-11-01
9 2007-12-01 2009-06-01
10 2020-02-01 2020-04-01

Before we draw, we find the smallest and the largest TFP growth rate: the title of the chart reports them.

# The range of TFP growth, in percent: `.min()` and `.max()` are the smallest and
# the largest value of the column tfp_growth
low, high = tfp.tfp_growth.min(), tfp.tfp_growth.max()
Show the drawing code
# @title Drawing code: Fig. 1, U.S. TFP growth and the recessions
def shade_recessions(fig):
    """Shade every NBER recession on a chart with years on the horizontal axis.

    Used by Fig. 1 (inspired by Fig. 15.1 in Jones); the bands come from
    recession_bands.

    Arguments
        fig : the Plotly figure to shade
    Returns
        the same figure
    """
    for start, end in recession_bands(recessions):
        # `add_vrect` shades the band between two x values
        fig.add_vrect(x0=start, x1=end, fillcolor=BLUE, opacity=0.12, line_width=0)
    return fig


# Step 1: `px.line` draws a line chart from two columns of the table, and
# `labels={...}` gives the columns readable names on the axes
fig = px.line(
    tfp,
    x="year",
    y="tfp_growth",
    markers=True,
    labels={"year": "Year", "tfp_growth": "TFP growth (percent per year)"},
    title=f"U.S. TFP growth swings between {low:.1f} and +{high:.1f} percent a year",
)
# Step 2: `update_traces` restyles the line: blue, with small dots
fig.update_traces(line=dict(color=BLUE, width=2), marker=dict(size=5))
# a dashed line at zero growth; `add_hline` draws a horizontal line
fig.add_hline(y=0, line_dash="dash", line_color=GRAY)
# the recessions, as shaded bands
shade_recessions(fig)
# Step 3: fixed axes
fig.update_xaxes(range=[1948, 2027])
fig.update_yaxes(range=[-3, 5], tickvals=[-2, -1, 0, 1, 2, 3, 4])
fig.show()

Sources. Data: John Fernald, Federal Reserve Bank of San Francisco, A Quarterly, Utilization-Adjusted Series on Total Factor Productivity, utilization-adjusted TFP growth of the U.S. business sector (dtfp_util, sheet annual, 2026 vintage); recession dates: NBER, U.S. Business Cycle Expansions and Contractions, as the NBER-based Recession Indicator (USREC), via FRED. Prepared files: data/ch15/fig15-1.csv and data/ch15/recessions.csv (see the data notes). Chart: inspired by Fig. 15.1 in Jones (2024), Macroeconomics, 6th ed.

What to notice.

  • TFP growth is far from constant: it ranges from −2.4 percent (1993) to +4.0 percent (1953), and it is negative in 16 of the 73 years from 1950 to 2022. Shocks to TFP are real and large.
  • But they do not line up with the shaded recessions. In 2001 and 2020 TFP grew by 2.3 and 1.8 percent, and in 2009, the worst year of the Great Recession, by 3.5 percent, one of the largest values in the series.
  • So TFP shocks cannot be the whole story of business cycles. That is one reason DSGE models added many other shocks (monetary policy, government purchases, financial frictions) and frictions such as sticky prices.

Which TFP? The raw Solow residual and the utilization-adjusted series

The textbook caption says the series comes “from the Solow growth accounting formula”. That is not quite right, and the difference matters. In a recession, firms keep some idle workers and machines, or use them less intensively. Plain growth accounting does not see this, so the slack shows up as a fall in TFP. Fernald estimates this change in utilization and removes it (equation 4); the textbook’s line is the utilization-adjusted series. The wider file tfp_annual.csv holds both, with the growth of business output:

# Load the wider file: business output growth, the raw Solow residual, utilization
# and adjusted TFP growth, one row per year.
# `annual.year.isin([...])` is True for the years in the list, so we see four rows
annual = pd.read_csv(DATA_URL + "tfp_annual.csv")
annual[annual.year.isin([2001, 2008, 2009, 2020])]
year gdp_growth tfp_solow utilization tfp_util
51 2001 0.7128 -0.1348 -2.4831 2.3491
58 2008 -1.7041 -2.0614 -1.3409 -0.7189
59 2009 -3.7508 -0.1489 -3.6778 3.5271
70 2020 -2.9012 -0.2302 -2.0451 1.8088

In 2009 output fell by 3.75 percent. The raw Solow residual (tfp_solow) fell slightly, −0.15 percent, but utilization fell by 3.68 percent, so the adjusted TFP growth (tfp_util) is +3.53 percent. To compare the two measures in one chart, we stack them in one long table.

# Step 1: `melt` turns columns into rows: the two TFP columns become one column,
# tfp_growth, and a new column, measure, says which series each row comes from
long = annual.melt(
    id_vars=["year", "gdp_growth"],
    value_vars=["tfp_solow", "tfp_util"],
    var_name="measure",
    value_name="tfp_growth",
)
# Step 2: `.map({...})` replaces each code with a readable name
long["measure"] = long.measure.map(
    {
        "tfp_solow": "Raw Solow residual",
        "tfp_util": "Utilization-adjusted TFP (textbook)",
    }
)
# Step 3: `np.where(condition, a, b)` gives a where the condition holds and b
# elsewhere: here, whether GDP fell or grew that year
long["GDP"] = np.where(long.gdp_growth < 0, "GDP fell", "GDP grew")
long.head()
year gdp_growth measure tfp_growth GDP
0 1950 9.4019 Raw Solow residual 7.1698 GDP grew
1 1951 5.8361 Raw Solow residual 2.2484 GDP grew
2 1952 3.3189 Raw Solow residual 1.8537 GDP grew
3 1953 4.7295 Raw Solow residual 2.4164 GDP grew
4 1954 -1.0768 Raw Solow residual -0.1047 GDP fell

Next we measure how closely each TFP series moves with GDP: the correlation of the two growth rates, and a least-squares line for each measure.

# Step 1: `.corr()` is the correlation of two columns, between -1 and 1
r_raw = annual.gdp_growth.corr(annual.tfp_solow)
r_util = annual.gdp_growth.corr(annual.tfp_util)


def least_squares_line(measure):
    """The least-squares line of a TFP measure on GDP growth (Fig. 1, which TFP).

    TFP growth = intercept + slope * GDP growth, fitted to the years with data.

    Arguments
        measure : a column of annual, "tfp_solow" or "tfp_util"
    Returns
        slope, intercept : the slope and the intercept of the line
    """
    # the years that have both growth rates (`.dropna()` drops the others)
    points = annual[["gdp_growth", measure]].dropna()
    # `np.polyfit(x, y, 1)` fits a straight line by least squares and returns its
    # slope and intercept
    slope, intercept = np.polyfit(points.gdp_growth, points[measure], 1)
    return slope, intercept


# Step 2: the two ends of each line, at the edges of the horizontal axis
x_line = np.array([-4.5, 10])
Show the drawing code
# @title Drawing code: Fig. 1, raw and adjusted TFP against GDP growth
# Step 1: one point per year; `facet_col="measure"` draws one panel per measure,
# and the color says whether GDP fell
fig = px.scatter(
    long,
    x="gdp_growth",
    y="tfp_growth",
    facet_col="measure",
    color="GDP",
    hover_name="year",
    color_discrete_map={"GDP grew": BLUE, "GDP fell": GREEN},
    labels={"gdp_growth": "GDP growth (%)", "tfp_growth": "TFP growth (%)"},
    range_x=[-4.5, 10],
    range_y=[-5, 7.5],
    title=(
        f"Raw TFP moves with GDP (r = {r_raw:.2f}); "
        f"adjusted TFP does not (r = {r_util:.2f})"
    ),
)
# Step 2: the least-squares line of each panel, fitted to that panel's points
# only; `add_scatter(..., row=1, col=k)` adds it to panel k (1 = raw, 2 = adjusted)
for k, measure in enumerate(["tfp_solow", "tfp_util"], start=1):
    slope, intercept = least_squares_line(measure)
    fig.add_scatter(
        x=x_line,
        y=intercept + slope * x_line,
        mode="lines",
        line_color=GRAY,
        name="Least-squares line",
        showlegend=(k == 1),           # one legend entry for both lines
        row=1,
        col=k,
    )
# Step 3: Plotly writes the panel titles as "measure=..."; `.split("=")[-1]`
# keeps the part after the "="
for annotation in fig.layout.annotations:
    annotation.text = annotation.text.split("=")[-1]
# Step 4: the tick marks; no tick labels in the bottom-left corner
fig.update_xaxes(tickvals=[-2, 0, 2, 4, 6, 8])
fig.update_yaxes(tickvals=[-4, -2, 0, 2, 4, 6])
# `legend_title_text=""` removes the legend's title
fig.update_layout(legend_title_text="")
fig.show()

Sources. Data: John Fernald, Federal Reserve Bank of San Francisco, A Quarterly, Utilization-Adjusted Series on Total Factor Productivity, U.S. business sector, sheet annual: output growth (dY), the raw Solow residual (dtfp), utilization (dutil) and utilization-adjusted TFP growth (dtfp_util), 2026 vintage. Prepared file: data/ch15/tfp_annual.csv (see the data notes). Chart: inspired by Fig. 15.1 in Jones (2024), Macroeconomics, 6th ed., and its note on growth accounting.

What to notice. The raw Solow residual rises and falls with GDP: this strong co-movement was the original evidence for RBC models. After the utilization adjustment the correlation is about zero. Measured TFP falls in recessions partly because firms use their workers and machines less, not the other way around.

Beyond the United States: TFP growth in 120 countries

Fernald’s utilization-adjusted series exists only for the United States: it needs detailed data on hours per worker and industry inputs that few countries publish. The Penn World Table (PWT 11.0) measures TFP for 120 countries with plain growth accounting, so its TFP is a raw Solow residual, like tfp_solow above. For the United States, PWT’s TFP growth has a correlation of 0.93 with Fernald’s raw residual and only 0.29 with the adjusted series.

# Load the PWT growth rates and look at Japan's last three years.
# `world[world.country == "Japan"]` keeps the rows of one country
world = pd.read_csv(DATA_URL + "pwt_growth.csv")
world[world.country == "Japan"].tail(3)
iso3 country region year tfp_growth gdp_growth
3184 JPN Japan East Asia & Pacific 2021 1.465 2.527
3185 JPN Japan East Asia & Pacific 2022 -0.234 0.950
3186 JPN Japan East Asia & Pacific 2023 0.731 1.665

Each row is one country in one year: tfp_growth and gdp_growth are the growth rates of TFP and of real GDP in percent (100 times the change in the log), and region is the World Bank region. The chart marks the years in which real GDP fell, so you can check the RBC story country by country: does TFP fall when GDP falls?

First we compute, for one country, the years in which real GDP fell and how many of them also saw TFP fall. The website cannot run Python, so after the widget a helper cell precomputes the chart for every country and joins them with a menu.

def gdp_fall_years(country):
    """The years in which a country's GDP fell, and how often TFP fell too (Fig. 1).

    The RBC check, country by country: does TFP fall when GDP falls?

    Arguments
        country : the country's name, as in the column country of world
    Returns
        data : the country's rows of world
        fell : the rows of the years in which real GDP fell
        hit  : the number of those years in which TFP growth was negative too
    """
    # Step 1: the rows of one country, then the years in which GDP fell
    data = world[world.country == country]
    fell = data[data.gdp_growth < 0]
    # Step 2: `fell.tfp_growth < 0` is True or False for each year, and `.sum()`
    # counts the True values
    hit = (fell.tfp_growth < 0).sum()
    return data, fell, hit
Show the drawing code
# @title Drawing code: Fig. 1 for any country, TFP growth and the years GDP fell
def growth_axis(low, high):
    """A y-axis range that holds every point, the narrowest that fits.

    The choices are +-6, +-12, +-25, +-60 or +-110 percent, so the axis only
    widens when a country needs it.

    Arguments
        low, high : the smallest and the largest value to show
    Returns
        the range [bottom, top] of the axis
    """
    # try wider and wider axes; `return` stops at the first one that fits
    for top in [6, 12, 25, 60, 110]:
        if -top <= low and high <= top:
            return [-1.05 * top, 1.05 * top]


def tfp_country_chart(country="United States"):
    """TFP growth of one country, with the years in which real GDP fell marked.

    Draws Fig. 1 for any country (inspired by Fig. 15.1 in Jones), with raw TFP
    growth from PWT 11.0.

    Arguments
        country : the country's name
    Returns
        a Plotly figure
    """
    # Step 1: the numbers of the chart (gdp_fall_years);
    # `a if condition else b`: "year" for one year, "years" otherwise
    data, fell, hit = gdp_fall_years(country)
    years = "year" if len(fell) == 1 else "years"
    # Step 2: TFP growth year by year, a blue line with dots
    fig = px.line(
        data,
        x="year",
        y="tfp_growth",
        markers=True,
        color_discrete_sequence=[BLUE],
        labels={"year": "Year", "tfp_growth": "TFP growth (percent per year)"},
        title=(
            f"{country}: TFP also fell in {hit} of the {len(fell)} {years} "
            f"in which GDP fell"
        ),
    )
    fig.update_traces(
        name="TFP growth (PWT, raw)",
        showlegend=True,
        line_width=2,
        marker_size=5,
    )
    # Step 3: the years in which GDP fell, as green diamonds;
    # `go.Scatter(..., mode="markers")` adds points without a line
    fig.add_trace(
        go.Scatter(
            x=fell.year,
            y=fell.tfp_growth,
            mode="markers",
            name="A year in which GDP fell",
            marker=dict(color=GREEN, size=11, symbol="diamond"),
        )
    )
    # Step 4: zero growth, fixed years, and a y-axis that fits this country
    fig.add_hline(y=0, line_dash="dash", line_color=GRAY)
    fig.update_xaxes(range=[1953, 2025])
    fig.update_yaxes(range=growth_axis(data.tfp_growth.min(), data.tfp_growth.max()))
    # the legend in the bottom-left corner
    fig.update_layout(legend=dict(x=0.01, y=0.01, xanchor="left", yanchor="bottom"))
    return fig
# Fig. 1 for any country, with a menu. `interact` turns each argument of the
# function into a widget, here a `Dropdown` menu of the country names
# (`sorted(world.country.unique())`: each name once, in alphabetical order), and
# draws the figure again every time you pick one. The `;` at the end only hides
# an extra line of text.
interact(
    tfp_country_chart,
    country=Dropdown(options=sorted(world.country.unique()), value="United States",
                     description="country", style=WIDE),
);
Show the helper code
# @title Helper for the website country menu (run this cell; you do not need to read it)
def dropdown_figure(make_figure, name, values, start):
    """Precompute make_figure(name=value) for every value, joined by a dropdown menu.

    Like slider_figure, but each choice also sets its own axis ranges. Used by the
    website version of Fig. 1 for any country.

    Arguments
        make_figure : the figure function, e.g. tfp_country_chart
        name        : the argument that the menu sets, e.g. "country"
        values      : the choices of the menu
        start       : the choice shown first
    Returns
        a Plotly figure with a dropdown menu
    """
    # Step 1: draw the figure once for every value
    figures = [make_figure(**{name: v}) for v in values]
    first = values.index(start)
    # Step 2: copy every trace into one figure; only the start value's are visible.
    # `owner` remembers which value each trace belongs to
    fig = go.Figure(layout=figures[first].layout)
    owner = []
    for i, f in enumerate(figures):
        for trace in f.data:
            trace.visible = (i == first)
            fig.add_trace(trace)
            owner.append(i)
    # Step 3: one menu entry per value: its traces, its title and its axis ranges
    buttons = []
    for i, f in enumerate(figures):
        layout = {
            "title": f.layout.title.to_plotly_json(),
            "xaxis.range": f.layout.xaxis.range,
            "yaxis.range": f.layout.yaxis.range,
        }
        buttons.append(
            dict(
                method="update",
                label=str(values[i]),
                args=[{"visible": [o == i for o in owner]}, layout],
            )
        )
    # Step 4: the menu, just above the top-right corner of the chart
    fig.update_layout(
        updatemenus=[
            dict(
                buttons=buttons,
                active=first,
                direction="down",
                x=1,
                y=1.02,
                xanchor="right",
                yanchor="bottom",
            )
        ],
        margin=dict(t=110),
    )
    return fig
Show the code of the website version
# @title Website version: Fig. 1 for any country, with a country menu
# one picture of Fig. 1 per country; the menu shows one at a time
dropdown_figure(
    tfp_country_chart,
    "country",
    sorted(world.country.unique()),
    start="United States",
)

Sources. Data: Groningen Growth and Development Centre, Penn World Table 11.0 (doi:10.34894/FABVLR), TFP at constant national prices (rtfpna) and real GDP at constant national prices (rgdpna); please cite Feenstra, Robert C., Robert Inklaar and Marcel P. Timmer (2015), “The Next Generation of the Penn World Table,” American Economic Review 105(10): 3150–3182 (doi:10.1257/aer.20130954). Regions: World Bank country classifications. Prepared file: data/ch15/pwt_growth.csv (see the data notes). Chart: inspired by Fig. 15.1 in Jones (2024), Macroeconomics, 6th ed.

On the website, pick a country in the menu above the chart.

What to notice.

  • In most countries the raw Solow residual falls almost every time GDP falls: in Japan, Italy and Mexico in every such year, and in 930 of the 990 country-years with falling GDP in the whole table. That is the co-movement RBC models were built on, and Fernald’s adjustment suggests much of it is a change in utilization.
  • The United States is an exception: PWT’s TFP fell in only 4 of its 8 years of falling GDP, and it grew in 2009.
  • Germany, Japan, Italy and the United Kingdom all show TFP falls of 3.8 to 4.5 percent in 2009, when their GDP fell by 4.7 to 5.9 percent.
  • Look at your data. Pick Iraq or Kuwait: in 1991, the year of the Gulf War, their TFP growth is −105 and −51 percent (log changes, which can go below −100). Macao’s is −80 in 2020, when its casinos closed. These are collapses of output, not TFP shocks in the RBC sense, and they are why the axis widens for some countries.

Key numbers: TFP in recessions

The textbook says TFP growth is negative in some years, but not in 2001 or 2020, and that 2009 brought a big positive shock. We check these statements, and count how often each TFP measure fell in a year when GDP fell.

# Step 1: the years of negative TFP growth in the textbook's span.
# `tfp[tfp.year <= 2022]` keeps the rows for which the condition is True (a
# filter), and `len(...)` counts the rows
book_years = tfp[tfp.year <= 2022]
negative = book_years[book_years.tfp_growth < 0]
print(f"Years with negative TFP growth, 1950-2022: {len(negative)}")
print(list(negative.year))
# Step 2: the years in which business output fell, and how often each TFP measure
# fell too; `.sum()` of a True/False column counts the True values
fell = annual[annual.gdp_growth < 0]
print(f"Years in which business output fell: {len(fell)}")
print(f"  raw Solow residual also fell in {(fell.tfp_solow < 0).sum()} of them")
print(
    f"  utilization-adjusted TFP fell in {(fell.tfp_util < 0).sum()} of them: "
    f"{list(fell.year[fell.tfp_util < 0])}"
)
Years with negative TFP growth, 1950-2022: 16
[1959, 1969, 1974, 1979, 1983, 1987, 1993, 1994, 2006, 2007, 2008, 2010, 2011, 2013, 2016, 2022]
Years in which business output fell: 11
  raw Solow residual also fell in 9 of them
  utilization-adjusted TFP fell in 2 of them: [1974, 2008]

These are current data. Fernald revises his series as the BEA and BLS revise theirs, so recent years differ from the textbook’s vintage: 2021 is 1.26 percent here, about 0.45 percent in the textbook’s figure.

Your turn. Find the five years with the lowest utilization-adjusted TFP growth. Were any of them recession years? Then do the same for the raw Solow residual with annual.

Hint

tfp.nsmallest(5, "tfp_growth") gives the five rows with the smallest values. Compare their years with recessions; for the raw residual use annual.nsmallest(5, "tfp_solow").

# Your turn: try it here

The labor market in the DSGE model

Here is the diagram at the heart of the chapter: labor demand from firms, labor supply from households, and the equilibrium where they cross.

Fig. 2: The labor market in the DSGE model

Goal. Draw the labor market of the DSGE model and find its equilibrium.

The labor demand curve slopes down because the marginal product of labor falls as firms hire more workers. The labor supply curve slopes up because people work more when the wage is higher. They cross at employment \(L_0\) and the real wage \(w_0\) (equations 5 and 6):

\[L^d:\ w = \frac{2}{3}\cdot\frac{\bar{A}K^{1/3}}{L^{1/3}} \qquad\qquad L^s = \bar{\ell}\cdot\frac{w}{c}\]

The parameter \(\bar{\ell}\) sets the overall amount of labor supply: when \(c = w\), people supply exactly \(L^s = \bar{\ell}\). Move the sliders to see which curve each parameter shifts.

The function labor_market draws the two curves and marks where they cross. It only calls equilibrium and the curves of Model in code, so all of its code is drawing code.

Show the drawing code
# @title Drawing code: Fig. 2, the labor market
def labor_market(A=A_BAR, c=C_BAR, ell=ELL_BAR):
    """Labor demand and labor supply cross at employment L0 and the real wage w0.

    Draws Fig. 2 (adapted from Fig. 15.2 in Jones).

    Arguments
        A   : TFP, A-bar
        c   : consumption, c
        ell : the labor-supply scale, ell-bar
    Returns
        a Plotly figure
    """
    # Step 1: the equilibrium, equation (7), with no sales tax
    L0, w0 = equilibrium(A, 0.0, c, ell)
    # Step 2: the two curves; the supply curve's name is left out when it would sit
    # on the demand curve's
    fig = go.Figure()
    d_end = add_curve(fig, L_GRID, demand_wage(L_GRID, A), "L<sup>d</sup>", BLUE)
    add_curve(fig, L_GRID, supply_wage(L_GRID, c, ell), "L<sup>s</sup>", TEAL,
              avoid=d_end)
    # Step 3: the equilibrium point, with guides to both axes
    add_point(fig, L0, w0, "")
    # Step 4: the title reports L0, w0 and output
    title = (
        f"Equilibrium: employment L₀ = {L0:.2f}, real wage w₀ = {w0:.2f}, "
        f"output Y = {output(L0, A):.2f}"
    )
    return labor_axes(fig, title)
# Fig. 2 with sliders. A `FloatSlider` is a slider for a decimal number: its start
# value, range and step, then its label
interact(
    labor_market,
    A=FloatSlider(value=1.0, min=0.6, max=1.3, step=0.05,
                  description="TFP Ā", style=WIDE),
    c=FloatSlider(value=0.6, min=0.5, max=0.8, step=0.05,
                  description="consumption c", style=WIDE),
    ell=FloatSlider(value=0.9, min=0.5, max=1.2, step=0.1,
                    description="labor-supply scale ℓ̄", style=WIDE),
);
Show the code of the website version
# @title Website version: Fig. 2 with a precomputed slider for consumption
# one picture of Fig. 2 per value of c
slider_figure(
    labor_market,
    "c",
    [0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8],     # the values of c
    start=0.6,
    label="consumption c",
)

Source. Adapted from Fig. 15.2 in Jones (2024), Macroeconomics, 6th ed.

What to notice.

  • In the baseline the market clears at \(L_0 = 1.00\) and \(w_0 = 0.67\).
  • \(\bar{A}\) appears only in labor demand, so it shifts only the demand curve. Consumption \(c\) and \(\bar{\ell}\) appear only in labor supply. Higher consumption shifts labor supply up and to the left: richer people want more leisure, so they work less at any wage.
  • Labor demand is curved and quite flat at the right: a 1 percent lower wage raises the labor firms want by 3 percent (in \(L = (\tfrac{2}{3}\bar{A}K^{1/3}/w)^3\) the wage has a power of 3).

Your turn. Raise \(\bar{\ell}\) from 0.9 to 1.2. Which curve shifts, and in which direction? What happens to employment and the real wage? Check your answer with equilibrium(ell=1.2).

Hint

\(\bar{\ell}\) enters \(L^s = \bar{\ell}\,w/c\) in the same way as \(1/c\), so raising \(\bar{\ell}\) has the same effect as lowering consumption.

# Your turn: try it here

Real shocks: productivity, taxes and government purchases

We shift one curve at a time: lower TFP and a sales tax move labor demand, while more government purchases work through consumption and labor supply. Notes in these sections also describe the matching responses in a large estimated DSGE model.

Fig. 3: A negative TFP shock in the DSGE model

Goal. See why a fall in TFP looks like a recession in the labor market.

A temporary fall in TFP from \(\bar{A}\) to \(\bar{A}'\) lowers the marginal product of labor at every level of employment, so labor demand shifts down. Labor supply does not contain \(\bar{A}\), and because the shock is temporary it barely changes consumption, so the supply curve stays put. The economy moves from \(A\) to \(B\).

This figure function will serve four textbook figures: a TFP shock (here), a sales tax (Fig. 4), government purchases (Fig. 5) and good news about the future (Fig. 9). Its three arguments are the three things a shock can change: TFP \(\bar{A}'\), the sales tax \(\tau\) and consumption \(c'\). When a curve moves, its old position stays behind as a dashed gray line.

labor_shock only calls equilibrium and the curves of Model in code, so all of its code is drawing code: open it to see how the curves, the points and the arrows are placed.

Show the drawing code
# @title Drawing code: Figs. 3, 4, 5 and 9, a shock in the labor market
def labor_shock(A_new=A_BAR, tau=0.0, c_new=C_BAR):
    """A shock to TFP, a sales tax or consumption moves the economy from A to B.

    Draws Figs. 3, 4, 5 and 9 (adapted from Fig. 15.3 to Fig. 15.5 and Fig. 15.14
    in Jones). A curve that moves leaves its old position behind as a dashed line.

    Arguments
        A_new : TFP after the shock, A-bar'
        tau   : the sales tax, tau
        c_new : consumption after the shock, c'
    Returns
        a Plotly figure
    """
    # Step 1: the equilibrium before (A) and after (B) the shock, equation (7)
    L0, w0 = equilibrium()
    L1, w1 = equilibrium(A_new, tau, c_new)
    # Step 2: which curves move? Slider values are not always exact (0.6 can arrive
    # as 0.6000000000000001), so we ask whether a value differs from the baseline by
    # more than 0.001 rather than testing `!=`; `abs(x)` is the absolute value
    demand_moves = abs(A_new - A_BAR) > 0.001 or tau > 0.001
    supply_moves = abs(c_new - C_BAR) > 0.001
    # a prime marks a curve that moved; `a if condition else b` picks a when the
    # condition holds and b otherwise
    d_label = "L<sup>d</sup>′" if demand_moves else "L<sup>d</sup>"
    s_label = "L<sup>s</sup>′" if supply_moves else "L<sup>s</sup>"
    # Step 3: the curves after the shock
    fig = go.Figure()
    d_end = add_curve(fig, L_GRID, demand_wage(L_GRID, A_new, tau), d_label, BLUE)
    s_end = add_curve(fig, L_GRID, supply_wage(L_GRID, c_new), s_label, TEAL,
                      avoid=d_end)
    # Step 4: a curve that moved: its old position (dashed gray) and an arrow
    if demand_moves:
        add_curve(fig, L_GRID, demand_wage(L_GRID), "L<sup>d</sup>", GRAY, "dash",
                  avoid=d_end)
        add_arrow(fig, 1.45, demand_wage(1.45), 1.45, demand_wage(1.45, A_new, tau))
    if supply_moves:
        add_curve(fig, L_GRID, supply_wage(L_GRID), "L<sup>s</sup>", GRAY, "dash",
                  avoid=s_end)
        # low enough that the arrow ends inside the axes
        w_arrow = min(0.95, 1.75 * c_new / ELL_BAR)
        add_arrow(fig, labor_supply(w_arrow), w_arrow, labor_supply(w_arrow, c_new),
                  w_arrow)
    # Step 5: the points A and B, and arrows along both axes
    add_point(fig, L0, w0, "A")
    if demand_moves or supply_moves:
        add_point(fig, L1, w1, "B")
        add_arrow(fig, 0.05, w0, 0.05, w1)          # the real wage moves ...
        add_arrow(fig, L0, 0.05, L1, 0.05)          # ... and so does employment
    # Step 6: the title reports both changes
    title = f"Employment {L0:.2f} → {L1:.2f},   real wage {w0:.2f} → {w1:.2f}"
    return labor_axes(fig, title)
# Fig. 3 with sliders: TFP after the shock, the sales tax and consumption
interact(
    labor_shock,
    A_new=FloatSlider(value=0.8, min=0.6, max=1.4, step=0.05,
                      description="TFP after the shock Ā′", style=WIDE),
    tau=FloatSlider(value=0.0, min=0.0, max=0.5, step=0.05,
                    description="sales tax τ", style=WIDE),
    c_new=FloatSlider(value=0.6, min=0.4, max=0.8, step=0.05,
                      description="consumption c′", style=WIDE),
);
Show the code of the website version
# @title Website version: Fig. 3 with a precomputed slider for TFP
# one picture of Fig. 3 per value of A-bar'
slider_figure(
    labor_shock,
    "A_new",
    [0.7, 0.8, 0.9, 1.0, 1.1, 1.2],     # the values of A-bar'
    start=0.8,
    label="TFP after the shock Ā′",
)

Source. Adapted from Fig. 15.3 in Jones (2024), Macroeconomics, 6th ed.

What to notice.

  • With \(\bar{A}' = 0.8\), employment falls from 1.00 to 0.85 and the real wage from 0.67 to 0.56. Both fall, just as in a recession.
  • Both fall by the same 15 percent. In equation (7), TFP has the power \(3/4\), so \(0.8^{3/4} = 0.85\); and the wage is proportional to employment along the unchanged supply curve.
  • Move \(\bar{A}'\) above 1: a positive TFP shock raises employment and the wage, like a boom. This is why the first DSGE models focused on TFP shocks.

Your turn. How large a fall in TFP cuts employment by exactly 10 percent? Find it with the slider, then compute it with the formula.

Hint

You need \((\bar{A}')^{3/4} = 0.9\). Raise both sides to the power \(4/3\): try 0.9 ** (4 / 3), then check with equilibrium(A=...).

# Your turn: try it here

Textbook Fig. 15.11: a TFP shock in an estimated DSGE model

Textbook Fig. 15.11 is not recomputed in this notebook. It shows how GDP, consumption, hours worked and inflation respond, quarter by quarter for five years, to a 1 percent rise in TFP in the estimated model of Frank Smets and Rafael Wouters, “Shocks and Frictions in U.S. Business Cycles: A Bayesian DSGE Approach,” American Economic Review 97(3), 2007, pp. 586–606 (doi:10.1257/aer.97.3.586). Its curves are the output of a large model, with sticky prices and wages, many shocks and dozens of parameters estimated from U.S. data, solved with specialized software. They are not data that we could download, and reading numbers off the printed curves would invent a precision that we do not have. So we only report what the textbook says: GDP rises by more than 1 percent at its peak, because capital accumulates as in the Solow model; consumption rises by much less, because people smooth it; hours worked fall at first, because with sticky prices firms need fewer workers to meet a demand that rises only slowly; and inflation falls.

The one number we can check is the persistence of the shock. The textbook says that after 4 years (16 quarters) the TFP shock has decayed to about half its size. If a shock keeps the fraction \(\rho\) of its size each quarter, then by equation (1) \(\rho^{16} = 1/2\):

# Step 1: the quarterly persistence rho that halves the shock in 16 quarters
rho = 0.5 ** (1 / 16)
print(
    f"Quarterly persistence rho = {rho:.3f}: "
    f"the shock keeps {100 * rho:.1f}% of its size each quarter"
)
# Step 2: what is left after 1, 4 and 8 years (4, 16 and 32 quarters)
print(
    f"Left after 1 year: {100 * rho ** 4:.0f}%,  "
    f"after 4 years: {100 * rho ** 16:.0f}%,  "
    f"after 8 years: {100 * rho ** 32:.0f}%"
)
Quarterly persistence rho = 0.958: the shock keeps 95.8% of its size each quarter
Left after 1 year: 84%,  after 4 years: 50%,  after 8 years: 25%

Fig. 4: The imposition of a sales tax in the DSGE model

Goal. See that a tax on firms’ sales works exactly like a fall in TFP.

With a sales tax \(\tau\), firms keep only \(1-\tau\) of each dollar of sales, so they hire until the after-tax marginal product equals the wage (equation 5):

\[w = (1-\tau)\cdot\frac{2}{3}\cdot\frac{\bar{A}K^{1/3}}{L^{1/3}}.\]

The tax shifts labor demand down, and labor supply stays put. We use the same function as in Fig. 3, now with the tax slider set to 20 percent and TFP at its baseline.

# Fig. 4: the function of Fig. 3, with a 20 percent sales tax and TFP at baseline
interact(
    labor_shock,
    A_new=FloatSlider(value=1.0, min=0.6, max=1.4, step=0.05,
                      description="TFP Ā′", style=WIDE),
    tau=FloatSlider(value=0.2, min=0.0, max=0.5, step=0.05,
                    description="sales tax τ", style=WIDE),
    c_new=FloatSlider(value=0.6, min=0.4, max=0.8, step=0.05,
                      description="consumption c′", style=WIDE),
);
Show the code of the website version
# @title Website version: Fig. 4 with a precomputed slider for the sales tax
# one picture of Fig. 4 per value of tau
slider_figure(
    labor_shock,
    "tau",
    [0.0, 0.1, 0.2, 0.3, 0.4],     # the values of tau
    start=0.2,
    label="sales tax τ",
)

Source. Adapted from Fig. 15.4 in Jones (2024), Macroeconomics, 6th ed.

What to notice.

  • A 20 percent sales tax moves the economy to exactly the same point as a fall in TFP to 0.8 in Fig. 3: employment 0.85 and a real wage of 0.56. Both lower the term \((1-\tau)\bar{A}\) from 1 to 0.8.
  • So a recession-like fall in employment and wages does not require that people “forget” technology. A policy change, or another distortion that acts like a tax, can play the role of a negative TFP shock.

Key numbers: a tax and a TFP shock

# The equilibrium with a 20 percent sales tax, and with TFP down to 0.8:
# only the product (1 - tau) * A-bar matters, so the two are the same
L_tax, w_tax = equilibrium(tau=0.2)
L_tfp, w_tfp = equilibrium(A=0.8)
print(f"Sales tax 20%:   employment {L_tax:.4f},  real wage {w_tax:.4f}")
print(f"TFP falls to 0.8: employment {L_tfp:.4f},  real wage {w_tfp:.4f}")
Sales tax 20%:   employment 0.8459,  real wage 0.5639
TFP falls to 0.8: employment 0.8459,  real wage 0.5639

Your turn. Find the TFP level \(\bar{A}'\) that gives the same equilibrium as a 30 percent sales tax. Check it by comparing two calls to equilibrium.

Hint

Only the product \((1-\tau)\bar{A}\) matters. Compare equilibrium(tau=0.3) with equilibrium(A=...).

# Your turn: try it here

Fig. 5: A rise in government purchases

Goal. See how expectations about future taxes reach today’s labor market through consumption.

Government purchases appear in neither labor curve. But a rise in purchases must be paid for with taxes at some point, so households are poorer over their lifetimes, and by the permanent-income hypothesis they consume less today. Consumption is the variable that lets future events affect the economy now. In \(L^s = \bar{\ell}w/c\), a lower \(c\) raises labor supply at every wage: people are poorer, so they also “buy” less leisure. Labor supply shifts down and to the right. Here consumption falls from 0.6 to 0.5, which corresponds to a permanent rise in purchases of 0.1 (10 percent of output); a temporary rise would cut consumption by less.

# Fig. 5: the function of Fig. 3, with consumption down from 0.6 to 0.5
interact(
    labor_shock,
    A_new=FloatSlider(value=1.0, min=0.6, max=1.4, step=0.05,
                      description="TFP Ā′", style=WIDE),
    tau=FloatSlider(value=0.0, min=0.0, max=0.5, step=0.05,
                    description="sales tax τ", style=WIDE),
    c_new=FloatSlider(value=0.5, min=0.4, max=0.8, step=0.05,
                      description="consumption c′", style=WIDE),
);
Show the code of the website version
# @title Website version: Fig. 5 with a precomputed slider for consumption
# one picture of Fig. 5 per value of c'
slider_figure(
    labor_shock,
    "c_new",
    [0.4, 0.45, 0.5, 0.55, 0.6],     # the values of c'
    start=0.5,
    label="consumption c′",
)

Source. Adapted from Fig. 15.5 in Jones (2024), Macroeconomics, 6th ed.

What to notice.

  • Employment rises from 1.00 to 1.15, but the real wage falls, from 0.67 to 0.64, and so does consumption. With capital fixed, the extra workers run into diminishing returns.
  • The wage falls much less than employment rises, because labor demand is flat (Fig. 2).
  • A boom in employment with falling wages and consumption does not look like a normal business cycle. Shocks to government purchases are part of the story, but they cannot be its main driver.

Your turn. Compare a permanent rise in purchases, which cuts consumption to \(c' = 0.5\), with a temporary one that cuts it only to \(c' = 0.58\). How much of the rise in employment is left in the temporary case?

Hint

Use equilibrium(c=0.5) and equilibrium(c=0.58), and compare the rise in employment above 1 in each case.

# Your turn: try it here

Textbook Fig. 15.12: government purchases in the Smets–Wouters model

Textbook Fig. 15.12 is not recomputed in this notebook. It shows the responses of GDP, consumption, hours worked and inflation over five years to a temporary 1 percent rise in government purchases that decays slowly, in the Smets–Wouters (2007) model (doi:10.1257/aer.97.3.586). Like all the impulse responses of the chapter, its curves are the solution of a large estimated model, not data, so we do not redraw them by eye. The textbook reads them as the labor market above predicts: hours worked and GDP rise, consumption falls, and inflation barely moves. GDP rises by about 1 percent in the short run, a multiplier of about 1, and the effect fades as the extra spending ends.

Monetary policy with sticky wages and sticky prices

If all prices adjusted at once, money would not affect employment. Making wages or prices sticky changes that, and lets a monetary expansion raise employment and output.

Fig. 6: Sticky wages in the DSGE model

Goal. See how a nominal wage that does not fall creates involuntary unemployment.

Monetary policy needs a distinction between nominal and real variables. Let \(W\) be the nominal wage (dollars per hour) and \(P\) the price level, so that \(w = W/P\) is the real wage. Suppose the nominal wage is sticky: it is stuck at \(W = \bar{W}\). With the price level at \(P_0 = 1\) and \(\bar{W} = 0.75\), the real wage is 0.75, above the market-clearing wage of 0.67.

At that wage, people want to supply more labor than firms want to hire. Trade needs both sides, so the short side of the market decides (equation 8): firms hire along their demand curve, at point \(A\). The gap between the two curves is involuntary unemployment: people who would like to work at the going wage but cannot find a job. The unlabelled dot marks where the market would clear.

This figure function covers one event in three periods, and Fig. 7 reuses it:

  • period 0: the nominal wage is stuck at \(\bar{W}\) (textbook Fig. 15.6);
  • period 1: a monetary expansion raises the price level to \(P'\), and the real wage falls to \(\bar{W}/P'\) (Fig. 15.7);
  • period 2: eventually the nominal wage adjusts and the market clears again (this last step is not in the textbook figures; it is what happens once wages are flexible).

First the economics. sticky_wage_periods computes the real wage in each period and, by equation (8), employment and unemployment.

# The price level before the monetary expansion
P0 = 1.0


def sticky_wage_periods(P_new, W_bar):
    """Real wage, employment and unemployment in periods 0, 1 and 2 (Figs. 6, 7).

    The real wage w = W/P is W-bar/P0, then W-bar/P', then the market-clearing w*
    once the nominal wage has adjusted. Equation (8): employment is the short side
    of the market, L = min(L^d(w), L^s(w)); unemployment is L^s(w) - L^d(w) when
    that is positive.

    Arguments
        P_new : the price level after the monetary expansion, P'
        W_bar : the sticky nominal wage, W-bar
    Returns
        L_star, w_star : employment and the real wage where the market clears
        real_wages     : the real wage W/P in periods 0, 1 and 2 (a list)
        employment     : employment in each period (a list)
        unemployment   : unemployment in each period (a list)
    """
    # Step 1: the market-clearing point, and W/P in periods 0, 1 and 2
    L_star, w_star = equilibrium()
    real_wages = [W_bar / P0, W_bar / P_new, w_star]
    # Step 2: in each period the short side of the market decides employment;
    # `max(a, 0.0)` keeps unemployment from going below zero
    employment, unemployment = [], []
    for w in real_wages:
        employment.append(min(labor_demand(w), labor_supply(w)))
        unemployment.append(max(labor_supply(w) - labor_demand(w), 0.0))
    return L_star, w_star, real_wages, employment, unemployment
Show the drawing code
# @title Drawing code: Figs. 6 and 7, a wage line and the unemployment bracket
def add_wage_line(fig, w, label, color, dash="solid", above=False):
    """A horizontal line at the real wage w, labeled just right of the axes.

    Used by Fig. 6 and Fig. 7: the sticky real wage W-bar/P.

    Arguments
        fig   : the Plotly figure
        w     : the real wage, W/P
        label : the line's name
        color : its color
        dash  : "solid" or "dash"
        above : True to write the name above the line, False below it
    """
    # the line, from the vertical axis to the right edge of the chart
    fig.add_trace(
        go.Scatter(
            x=[0.02, L_RANGE[1]],
            y=[w, w],
            mode="lines",
            showlegend=False,
            hoverinfo="skip",
            line=dict(color=color, width=3, dash=dash),
        )
    )
    # its name, just right of the axes
    fig.add_annotation(
        x=L_RANGE[1],
        y=w,
        text=label,
        showarrow=False,
        xanchor="left",
        yanchor="bottom" if above else "top",
        font=dict(color=color, size=15),
    )


def add_gap(fig, L_from, L_to, w, text):
    """A bracket from L_from to L_to above the real wage w, with a text over it.

    Used by Fig. 6 and Fig. 7 for the unemployment gap, drawn clear of the point
    letters.

    Arguments
        fig          : the Plotly figure
        L_from, L_to : the two ends of the bracket, on the employment axis
        w            : the real wage
        text         : the text over the bracket
    """
    # the bracket: 0.1 above the wage line, with short ends down to 0.07
    fig.add_trace(
        go.Scatter(
            x=[L_from, L_from, L_to, L_to],
            y=[w + 0.07, w + 0.1, w + 0.1, w + 0.07],
            mode="lines",
            showlegend=False,
            hoverinfo="skip",
            line=dict(color="black", width=1.5),
        )
    )
    # the text, centered over the bracket
    fig.add_annotation(
        x=(L_from + L_to) / 2,
        y=w + 0.1,
        text=text,
        showarrow=False,
        yanchor="bottom",
        font=dict(size=13),
    )
Show the drawing code
# @title Drawing code: Figs. 6 and 7, sticky wages period by period
def letter_side(w, w_old, w_star):
    """Where to write a point's letter, clear of the old wage line w_old above it.

    Used by Fig. 6 and Fig. 7. A function may have several `return` lines: it stops
    at the first one it reaches.

    Arguments
        w      : the real wage at the point
        w_old  : the real wage of period 0
        w_star : the market-clearing real wage
    Returns
        a Plotly text position, such as "top right"
    """
    if w > w_old - 0.01:
        return "top right"             # no old line just above the point
    if w > w_star + 0.01:
        return "bottom left"           # the point lies on labor demand
    return "bottom right"              # the point lies on labor supply
Show the drawing code
# @title Drawing code: Figs. 6 and 7, the labor market with a sticky wage
def sticky_wages(t=0, P_new=1.05, W_bar=0.75):
    """The labor market with a sticky nominal wage, in period t.

    Draws Fig. 6 and Fig. 7 (adapted from Figs. 15.6 and 15.7 in Jones): in period 0
    the nominal wage is stuck at W-bar; in period 1 prices rise to P'; in period 2
    W has adjusted.

    Arguments
        t     : the period, 0, 1 or 2
        P_new : the price level after the monetary expansion, P'
        W_bar : the sticky nominal wage, W-bar
    Returns
        a Plotly figure
    """
    # Step 1: the numbers of the figure (sticky_wage_periods); `real_wages[t]` is
    # element number t of the list, counting from 0
    numbers = sticky_wage_periods(P_new, W_bar)
    L_star, w_star, real_wages, employment, unemployment = numbers
    labels = ["W̄/P<sub>0</sub>", "W̄/P′", "W′/P′"]
    w = real_wages[t]
    employed = employment[t]
    gap = unemployment[t]
    # Step 2: the two curves (the demand curve named at its top end, clear of the
    # wage line), and an unlabeled dot where the market would clear
    fig = go.Figure()
    add_curve(fig, L_GRID, demand_wage(L_GRID), "L<sup>d</sup>", BLUE, start=True)
    add_curve(fig, L_GRID, supply_wage(L_GRID), "L<sup>s</sup>", TEAL)
    add_point(fig, L_star, w_star, "", guides=False)
    # Step 3: after period 0, the old wage line and point A stay, with arrows
    if t > 0:
        w_A = real_wages[0]
        L_A = employment[0]
        # the two wage lines carry their names on their outer sides
        add_wage_line(fig, w_A, labels[0], GRAY, "dash", above=w_A >= w)
        add_point(fig, L_A, w_A, "A")
        add_arrow(fig, 0.05, w_A, 0.05, w)
        add_arrow(fig, L_A, 0.05, employed, 0.05)
    # Step 4: this period's wage line and point; `"ABC"[t]` is letter number t
    add_wage_line(fig, w, labels[t], GREEN, above=t > 0 and real_wages[0] < w)
    add_point(fig, employed, w, "ABC"[t], where=letter_side(w, real_wages[0], w_star))
    # Step 5: the unemployment bracket, when the gap is visible
    if gap > 0.02:
        add_gap(fig, labor_demand(w), labor_supply(w), w, "Unemployment")
    # Step 6: the title reports the period's numbers
    title = (
        f"Period {t}: real wage W/P = {w:.2f}, employment {employed:.2f}, "
        f"unemployment {gap:.2f}"
    )
    return labor_axes(fig, title, "Real wage, W/P")
# Fig. 6 with sliders. An `IntSlider` is a slider for whole numbers, here the period
interact(
    sticky_wages,
    t=IntSlider(value=0, min=0, max=2, description="period t", style=WIDE),
    P_new=FloatSlider(value=1.05, min=1.0, max=1.25, step=0.05,
                      description="new price level P′", style=WIDE),
    W_bar=FloatSlider(value=0.75, min=0.6, max=0.9, step=0.05,
                      description="sticky nominal wage W̄", style=WIDE),
);
Show the code of the website version
# @title Website version: Fig. 6 with a precomputed slider for the sticky wage
# one picture of Fig. 6 per value of W-bar
slider_figure(
    sticky_wages,
    "W_bar",
    [0.7, 0.75, 0.8, 0.85, 0.9],     # the values of W-bar
    start=0.75,
    label="sticky nominal wage W̄",
)

Source. Adapted from Fig. 15.6 in Jones (2024), Macroeconomics, 6th ed.

What to notice.

  • At the sticky real wage of 0.75, firms hire 0.70, while people would like to supply 1.125: unemployment is 0.42.
  • The higher \(\bar{W}\), the larger the gap. Labor demand reacts strongly to the wage, so even a modest excess of the wage over its market-clearing level costs many jobs.
  • Set \(\bar{W}\) below 0.67 and the picture flips: now labor supply is the short side, firms cannot hire all the workers they want, and there is no unemployment.

Your turn. Find the sticky nominal wage \(\bar{W}\) at which unemployment disappears, with \(P_0 = 1\).

Hint

Unemployment is zero when the real wage \(\bar{W}/P_0\) equals the market-clearing wage. Print equilibrium() to see that wage.

# Your turn: try it here

Fig. 7: A monetary expansion with sticky wages

Goal. See how monetary policy can raise employment when nominal wages are sticky.

A monetary expansion, a lower fed funds rate, raises spending. In this example only wages are sticky, so the other prices rise, from \(P_0 = 1\) to \(P' = 1.05\). With the nominal wage still at \(\bar{W}\), the real wage \(\bar{W}/P'\) falls. At the lower real wage firms want more workers and fewer people want to work, so employment rises from \(A\) to \(B\) and unemployment shrinks. This is the same function as in Fig. 6, now in period 1. Move the period slider to 2 to let the nominal wage adjust.

# Fig. 7: the function of Fig. 6, now in period 1
interact(
    sticky_wages,
    t=IntSlider(value=1, min=0, max=2, description="period t", style=WIDE),
    P_new=FloatSlider(value=1.05, min=1.0, max=1.25, step=0.05,
                      description="new price level P′", style=WIDE),
    W_bar=FloatSlider(value=0.75, min=0.6, max=0.9, step=0.05,
                      description="sticky nominal wage W̄", style=WIDE),
);
Show the code of the website version
# @title Website version: Fig. 7 with a precomputed slider for the period
# one picture of Fig. 7 per period: 0, 1 and 2
slider_figure(sticky_wages, "t", [0, 1, 2], start=1, label="period t")

Source. Adapted from Fig. 15.7 in Jones (2024), Macroeconomics, 6th ed.

What to notice.

  • A 5 percent rise in prices lowers the real wage from 0.75 to 0.71 and raises employment from 0.70 to 0.81; unemployment falls from 0.42 to 0.26.
  • The real wage and employment move in opposite directions. In actual business cycles, booms do not usually come with low real wages, so sticky wages alone cannot be the whole story of how monetary policy works.
  • In period 2 the nominal wage has caught up with prices, the real wage is back at 0.67 and the market clears: in the long run, money does not change employment (the classical dichotomy).

Your turn. Which price level \(P'\) removes unemployment completely in period 1? What happens to employment if prices rise even further?

Hint

You need \(\bar{W}/P' = 2/3\) with \(\bar{W} = 0.75\). Above that price level the real wage is below the market-clearing wage: which side of the market is short now?

# Your turn: try it here

Fig. 8: A monetary expansion with sticky prices

Goal. See why, with sticky prices, labor demand is vertical and a monetary expansion raises both employment and the real wage.

Now wages are flexible, but firms change their prices only slowly. At its sticky price, each firm agrees to supply whatever customers demand. So spending decides output \(Y\), and the production function decides how much labor firms must hire to produce it (equation 9):

\[Y = \bar{A}K^{1/3}L^{2/3} \quad\Longrightarrow\quad L = \left(\frac{Y}{\bar{A}K^{1/3}}\right)^{3/2}.\]

Labor demand no longer depends on the wage: it is a vertical line. We start from the flexible-price output \(Y_0 = 1\), so point \(A\) is the baseline. A monetary expansion raises the output demanded to \(Y' = 1.15\), labor demand shifts right, and to attract the extra workers the nominal wage must rise. With prices stuck, the real wage \(W/P\) rises too: the economy moves up along the labor supply curve to \(B\).

sticky_price_point computes the two points of the figure: the labor that firms need for the output demanded, from equation (9), and the real wage at which people supply it.

# Output demanded before the shock: the flexible-price output of the baseline
Y0 = 1.0


def sticky_price_point(Y_new, c_new):
    """Employment and the real wage before and after a demand shock (Figs. 8, 10).

    Equation (9): with sticky prices, firms hire the labor needed for the output
    demanded, L = (Y / (A-bar K^(1/3)))^(3/2). The real wage is read off the labor
    supply curve, equation (6), at that L.

    Arguments
        Y_new : the output demanded after the shock, Y'
        c_new : consumption after the shock, c'
    Returns
        L0, L1 : employment before (A) and after (B) the shock
        w0, w1 : the real wage W/P before and after the shock
    """
    # Step 1: the labor needed for the output demanded, before and after
    L0, L1 = labor_needed(Y0), labor_needed(Y_new)
    # Step 2: the real wage at which people supply exactly that labor
    w0, w1 = supply_wage(L0), supply_wage(L1, c_new)
    return L0, L1, w0, w1
Show the drawing code
# @title Drawing code: Figs. 8 and 10, a vertical labor demand curve
def add_vertical(fig, L, label, color, dash="solid"):
    """A vertical labor demand curve at L, labeled just above the top of the axes.

    Used by Fig. 8 and Fig. 10: with sticky prices, labor demand does not depend on
    the wage.

    Arguments
        fig   : the Plotly figure
        L     : employment, where the line stands
        label : the line's name ("" for none)
        color : its color
        dash  : "solid" or "dash"
    """
    # the line, from the bottom to the top of the axes
    fig.add_trace(
        go.Scatter(
            x=[L, L],
            y=[0.02, W_RANGE[1]],
            mode="lines",
            showlegend=False,
            hoverinfo="skip",
            line=dict(color=color, width=3, dash=dash),
        )
    )
    # its name, just above the axes
    fig.add_annotation(
        x=L,
        y=W_RANGE[1],
        text=label,
        showarrow=False,
        yanchor="bottom",
        font=dict(color=color, size=15),
    )
Show the drawing code
# @title Drawing code: Figs. 8 and 10, the labor market with sticky prices
def sticky_prices(Y_new=1.15, c_new=C_BAR):
    """The labor market with sticky prices: labor demand is a vertical line.

    Draws Fig. 8 and Fig. 10 (adapted from Figs. 15.8 and 15.15 in Jones): the line
    stands at the labor needed for the output demanded.

    Arguments
        Y_new : the output demanded after the shock, Y'
        c_new : consumption after the shock, c'
    Returns
        a Plotly figure
    """
    # Step 1: the numbers of the figure (sticky_price_point)
    L0, L1, w0, w1 = sticky_price_point(Y_new, c_new)
    # Step 2: which curves move (by more than 0.001)? A prime marks a curve that moved
    demand_moves, supply_moves = abs(Y_new - Y0) > 0.001, abs(c_new - C_BAR) > 0.001
    d_label = "L<sup>d</sup>′" if demand_moves else "L<sup>d</sup>"
    s_label = "L<sup>s</sup>′" if supply_moves else "L<sup>s</sup>"
    # Step 3: labor demand now, and its old position dashed gray (without a name
    # when the two lines are too close for two names)
    fig = go.Figure()
    add_vertical(fig, L1, d_label, BLUE)
    if demand_moves:
        add_vertical(fig, L0, "L<sup>d</sup>" if abs(L1 - L0) > 0.1 else "", GRAY,
                     "dash")
    # Step 4: labor supply now, and its old position when it moved
    end = add_curve(fig, L_GRID, supply_wage(L_GRID, c_new), s_label, TEAL)
    if supply_moves:
        add_curve(fig, L_GRID, supply_wage(L_GRID), "L<sup>s</sup>", GRAY, "dash",
                  avoid=end)
    # Step 5: the points A and B, and arrows along both axes
    add_point(fig, L0, w0, "A")
    if demand_moves or supply_moves:
        add_point(fig, L1, w1, "B", where="bottom right")   # no curve passes there
        add_arrow(fig, L0, 0.05, L1, 0.05)
        add_arrow(fig, 0.05, w0, 0.05, w1)
    # Step 6: the title reports output, employment and the real wage
    title = (
        f"Output {Y0:.2f} → {Y_new:.2f}: employment {L0:.2f} → {L1:.2f}, "
        f"real wage {w0:.2f} → {w1:.2f}"
    )
    return labor_axes(fig, title, "Real wage, W/P")
# Fig. 8 with sliders: the output demanded and consumption
interact(
    sticky_prices,
    Y_new=FloatSlider(value=1.15, min=0.8, max=1.3, step=0.05,
                      description="output demanded Y′", style=WIDE),
    c_new=FloatSlider(value=0.6, min=0.4, max=0.7, step=0.05,
                      description="consumption c′", style=WIDE),
);
Show the code of the website version
# @title Website version: Fig. 8 with a precomputed slider for the output demanded
# one picture of Fig. 8 per value of Y'
slider_figure(
    sticky_prices,
    "Y_new",
    [0.9, 1.0, 1.05, 1.1, 1.15, 1.2, 1.3],     # the values of Y'
    start=1.15,
    label="output demanded Y′",
)

Source. Adapted from Fig. 15.8 in Jones (2024), Macroeconomics, 6th ed.

What to notice.

  • Output demanded rises by 15 percent, employment from 1.00 to 1.23 and the real wage from 0.67 to 0.82.
  • Employment and the real wage now move in the same direction, as they do over the business cycle. That is why models with sticky prices and monetary shocks, the new Keynesian models, became so important. Together with TFP shocks, they are the two “flavors” that modern DSGE models combine.
  • Employment rises more than output (by the power 3/2), because each extra worker adds less output than the last.

Your turn. With sticky prices, a rise in TFP can lower employment (textbook Exercise 2). Suppose TFP rises by 10 percent while the output demanded stays at \(Y = 1\). How much labor do firms need now?

Hint

labor_needed has a second argument: compare labor_needed(1.0) with labor_needed(1.0, A=1.1). This is the fall in hours worked that the textbook finds in Fig. 15.11.

# Your turn: try it here

Textbook Figs. 15.9, 15.10 and 15.13: monetary and financial shocks in the Smets–Wouters model

A full DSGE model reports impulse response functions: how one variable, such as GDP, responds over time to one shock, such as a rise in the fed funds rate. Textbook Figs. 15.9, 15.10 and 15.13 show such responses from the estimated model of Smets and Wouters, “Shocks and Frictions in U.S. Business Cycles: A Bayesian DSGE Approach,” American Economic Review 97(3), 2007, pp. 586–606 (doi:10.1257/aer.97.3.586). They are not recomputed here: they are the solution of a model with sticky prices and wages, investment adjustment costs and many shocks, whose parameters were estimated from U.S. data, and they cannot be rebuilt from public data series or read precisely off the page. What the textbook reports:

  • Fig. 15.9: the response of GDP to a temporary 1 percentage point rise in the fed funds rate. GDP is about 0.2 percent lower in the first quarter, the peak fall of about 0.33 percent comes 3 to 4 quarters after the shock, and most of the effect is gone after 5 years (20 quarters).
  • Fig. 15.10: the same shock, for GDP, consumption, hours worked and inflation. All four fall, as with sticky prices in Fig. 8 run in reverse. Consumption falls nearly as much as GDP, and inflation is slightly less than 0.2 percentage points lower for about 2 years.
  • Fig. 15.13: a financial friction shock that raises the interest rate households face by 1 percentage point above the fed funds rate. It looks much like a monetary policy shock, with its largest effect on consumption.

Good news about the future

Last, we look at news: people learn that the future will be better, raise their consumption today, and the labor market responds differently with flexible and with sticky prices.

Fig. 9: Good news in the DSGE model: flexible prices

Goal. See how news about the future changes the labor market today (textbook Exercise 1).

People learn of a discovery that will make the economy more productive in 5 years. Today nothing changes in production, so labor demand stays put. But people expect to be richer, so by the permanent-income hypothesis they consume more today: consumption rises from 0.6 to \(c' = 0.7\). Richer people want more of every good, including leisure, so labor supply shifts up and to the left.

The function good_news handles this event twice: with flexible prices it calls labor_shock from Fig. 3, and with sticky prices (Fig. 10) it calls sticky_prices from Fig. 8. With sticky prices, firms must also produce the extra consumption, so the output demanded rises by the same amount as consumption.

good_news only picks one of the two figure functions, so all of its code is drawing code. With sticky prices, it raises the output demanded by the rise in consumption.

Show the drawing code
# @title Drawing code: Figs. 9 and 10, good news about the future
def good_news(c_new=0.7, prices="flexible"):
    """Good news raises consumption today from 0.6 to c_new.

    Draws Fig. 9 and Fig. 10 (adapted from Figs. 15.14 and 15.15 in Jones), with
    flexible prices (labor_shock) or sticky prices (sticky_prices).

    Arguments
        c_new  : consumption after the news, c'
        prices : "flexible" or "sticky"
    Returns
        a Plotly figure
    """
    # flexible prices: only labor supply moves, as in Fig. 5
    if prices == "flexible":
        return labor_shock(c_new=c_new)
    # sticky prices: demand rises with consumption, Y' = Y0 + (c' - c)
    return sticky_prices(Y_new=Y0 + (c_new - C_BAR), c_new=c_new)
# Fig. 9 with a slider and buttons. `ToggleButtons` shows a row of buttons, one per
# option; good_news receives the text of the selected button
interact(
    good_news,
    c_new=FloatSlider(value=0.7, min=0.5, max=0.8, step=0.05,
                      description="consumption after the news c′", style=WIDE),
    prices=ToggleButtons(options=["flexible", "sticky"], value="flexible",
                         description="prices"),
);
Show the code of the website version
# @title Website version: Fig. 9 with a precomputed slider for consumption
# one picture of Fig. 9 per value of c'
slider_figure(
    good_news,
    "c_new",
    [0.5, 0.6, 0.7, 0.8],     # the values of c'
    start=0.7,
    label="consumption after the news c′",
)

Source. Adapted from Fig. 15.14 in Jones (2024), Macroeconomics, 6th ed., which illustrates the worked solution to its Exercise 1.

What to notice.

  • With flexible prices, the good news lowers employment today, from 1.00 to 0.89, and raises the real wage a little, from 0.67 to 0.69. People work less because they feel richer.
  • Nothing about technology has changed yet: the whole effect comes through expectations and consumption, the forward-looking variable of the model.

Your turn. Textbook Exercise 3 asks about a permanent rise in TFP. Then labor demand shifts out and consumption rises. Try a 10 percent rise in both, labor_shock(A_new=1.1, c_new=0.66). What happens to employment and to the real wage?

Hint

In equation (7), \(\bar{A}\) and \(c\) enter as the ratio \(\bar{A}/c\). What happens to that ratio when both rise by 10 percent?

# Your turn: try it here

Fig. 10: Good news in the DSGE model: sticky prices

Goal. Compare the same good news with sticky prices (textbook Exercise 1(d)).

With sticky prices, firms supply whatever is demanded. The good news raises consumption, so firms must produce more and need more workers: the vertical labor demand curve shifts right, from \(Y_0 = 1\) to \(Y' = 1.1\). At the same time labor supply still shifts up, because people feel richer. Firms must pay a higher wage to get the labor they need. Use the buttons (in Colab) or the slider (on the website) to switch between the two cases.

# Fig. 10: the function of Fig. 9, with the sticky-prices button selected
interact(
    good_news,
    c_new=FloatSlider(value=0.7, min=0.5, max=0.8, step=0.05,
                      description="consumption after the news c′", style=WIDE),
    prices=ToggleButtons(options=["flexible", "sticky"], value="sticky",
                         description="prices"),
);
Show the code of the website version
# @title Website version: Fig. 10 with a slider between flexible and sticky prices
# a slider with two steps: flexible prices, then sticky prices
slider_figure(
    good_news,
    "prices",
    ["flexible", "sticky"],
    start="sticky",
    label="prices",
)

Source. Adapted from Fig. 15.15 in Jones (2024), Macroeconomics, 6th ed., which illustrates the worked solution to its Exercise 1.

What to notice.

  • With sticky prices, employment rises, from 1.00 to 1.15, and the real wage rises a lot, from 0.67 to 0.90. With flexible prices (Fig. 9) employment fell to 0.89.
  • The same shock moves employment in opposite directions depending on whether prices are sticky. This is why macroeconomists argue so much about how sticky prices really are.

Your turn. Textbook Exercise 4: government purchases fall for a while. Households expect lower taxes, so consumption rises to 0.65, but total demand falls, to \(Y' = 0.95\). With sticky prices, what happens to employment and to the real wage? Use sticky_prices(Y_new=0.95, c_new=0.65).

Hint

The vertical demand curve moves left and labor supply moves up. Employment clearly falls; for the real wage the two shifts pull in opposite directions. The title rounds both wages to 0.67, so print them with more decimals: compare supply_wage(labor_needed(0.95), 0.65) with w0.

# Your turn: try it here

Concluding remarks

  • DSGE models are dynamic (they trace the economy over time), stochastic (shocks hit it) and general equilibrium (prices clear the markets). Their shocks fade as impulse responses, equation (1), and adjustment costs delay their peak effects.
  • They grew out of real business cycle models, which added TFP shocks to the Solow model, equation (2). TFP is measured as a Solow residual, equation (3); U.S. TFP growth swings a lot, but once utilization is removed, equation (4), it does not fall in most recessions.
  • The labor market is a key block of every DSGE model. Labor demand and supply, equations (5) and (6), give employment and the wage in equation (7). With consumption taken as given, a TFP fall or a sales tax lowers employment and the wage together, like a recession; a rise in government purchases or good news acts through consumption and labor supply instead.
  • Nominal rigidities decide how monetary policy works. With sticky wages, equation (8), an expansion raises employment but lowers the real wage; with sticky prices, equation (9), labor demand is vertical and an expansion raises both. Estimated DSGE models such as Smets–Wouters are quantitative: their impulse response functions say how much and how fast GDP, consumption, hours and inflation respond to each shock.
  • The next chapter studies consumption itself: the permanent-income hypothesis and the microfoundations behind the consumption that this chapter took as given.
  • For an advanced treatment, Appendix H solves the full Smets–Wouters model in Python and recomputes the impulse responses of textbook Figs. 15.9–15.13.

Practice problems

These ten problems put the chapter’s tools to work: conceptual problems apply its equations to real episodes, data-based problems send you back to Fernald’s data and the Penn World Table, and modeling problems answer policy questions with the labor-market diagrams. Each problem is marked Basic, Intermediate or Challenge. Try it on your own before you open the Hint and the Solution; the sliders of the modeling problems run in Colab (open the notebook with the badge at the top), and on the website you can use the website version of each figure above.

Conceptual problems

Problem 1 (Basic): How long do shocks last in an estimated DSGE model? Smets and Wouters (2007) estimated their model on quarterly U.S. data from 1966 to 2004. Each of its shocks fades like the shock process of the Model overview, \(x_{t+1} = \rho\,x_t + \varepsilon_{t+1}\). The table lists five of their estimates of \(\rho\) (the posterior modes in Table 1b of the working-paper version; the article is doi:10.1257/aer.97.3.586).

Shock What it moves \(\rho\) per quarter
productivity TFP 0.95
government spending public purchases 0.97
investment technology how much new capital each dollar of investment buys 0.71
risk premium the gap between the central bank’s rate and the return households require 0.18
monetary policy the interest rate, beyond what the central bank’s rule prescribes 0.12
  1. Use the half-life formula of the Model overview to compute the half-life of each shock, in quarters.

  2. Use equation (1) to compute the share of each shock that is still left one year (4 quarters) after it hits.

  3. The monetary policy shock is almost gone after a single quarter, yet the chapter reports that GDP falls most 3 to 4 quarters after a monetary shock. Which features of a DSGE model can give a short-lived shock long-lived effects?

Hint

The half-life is \(h = \ln 2/(-\ln\rho)\), and by equation (1) the share left after \(n\) quarters is \(\rho^n\); np.log gives the natural log. For (c), reread the frictions in “The building blocks of a DSGE model”, the adjustment-cost rule after equation (1), and ask how a central bank moves its interest rate from one quarter to the next.

Your answer:

# Your answer here
Solution

(a) and (b). For productivity, \(h = \ln 2/(-\ln 0.95) = 0.6931/0.0513 = 13.5\) quarters, and after a year \(0.95^4 = 0.81\) of the shock is left. The same two steps for every shock give:

Shock \(\rho\) Half-life, quarters Share left after a year, \(\rho^4\)
productivity 0.95 13.5, about 3.4 years 0.81
government spending 0.97 22.8, about 5.7 years 0.89
investment technology 0.71 2.0 0.25
risk premium 0.18 0.40 0.001
monetary policy 0.12 0.33 0.0002

The productivity estimate of 0.95 is close to the \(\rho = 0.958\) that the Model overview derived from a half-life of 16 quarters. In Python:

shocks = {"productivity": 0.95, "government spending": 0.97, "investment technology": 0.71,
          "risk premium": 0.18, "monetary policy": 0.12}
for name, rho_shock in shocks.items():
    half_life = np.log(2) / -np.log(rho_shock)
    print(f"{name:22s} half-life {half_life:5.2f} quarters, left after a year {rho_shock ** 4:.4f}")

shocks.items() walks through the pairs of a dictionary, one name and one value at a time.

(c) A shock and its effects are not the same thing. Three features spread a short shock over many quarters:

  • Adjustment costs. With the rule \(y_t = y_{t-1} + \lambda(x_t - y_{t-1})\), the economy closes only part of the gap each quarter, so its response keeps growing after the shock has faded. With \(\rho = 0.8\) and \(\lambda = 0.3\) it peaks 3 quarters later (the Model overview’s check).
  • Sticky prices and wages. Prices adjust slowly, so a higher nominal interest rate is also a higher real rate for several quarters, and spending stays low.
  • A smooth policy rule. In the estimated rule the interest rate puts a weight of 0.81 on its own value of the quarter before (Table 1a of the same paper), so a rate that jumps comes back down only slowly, long after the shock itself is gone.

So the long swings of a DSGE model come either from shocks that are very persistent themselves (productivity, government spending) or from frictions that stretch short-lived shocks, like the monetary one, over many quarters.

Problem 2 (Intermediate): Two recessions, two stories: 1974 and 1982. John Fernald’s data in tfp_annual.csv (Federal Reserve Bank of San Francisco) give, for the U.S. business sector (percent per year):

Year Output growth Raw Solow residual Utilization
1974 −1.56 −3.72 −2.62
1982 −2.31 −2.76 −2.94
  1. Use equation (4) to compute true TFP growth in each year.

  2. In 1973–74 an oil embargo nearly quadrupled the price of oil, from $2.90 to $11.65 a barrel (Federal Reserve History). Our production function has no oil in it. Explain why a jump in the price of oil can act like a fall in \(\bar{A}\), and use the table “Which way do employment and the real wage move?” of the Model overview to say what the flexible-price labor market predicts.

  3. In 1981–82 the Federal Reserve tightened money until the federal funds rate approached 20 percent, and unemployment reached nearly 11 percent late in 1982 (Federal Reserve History). Which row of the same table fits 1982? Why did the raw Solow residual fall that year anyway?

  4. What do the two years say about the claim that TFP shocks drive business cycles?

Hint

Equation (4) says raw residual = true TFP growth + utilization growth, so subtract. For (b), think about what the same workers and machines can produce once energy is much more expensive. For (c), a monetary contraction is a monetary expansion run in reverse; which friction lets money change employment?

Your answer:

Solution

(a) From equation (4), \(g_A = (\text{raw residual}) - g_u\):

  • 1974: \(g_A = -3.72 - (-2.62) = -1.10\) percent (Fernald’s adjusted series has \(-1.09\); the identity holds only approximately for annual averages);
  • 1982: \(g_A = -2.76 - (-2.94) = +0.18\) percent.

True TFP fell in 1974 and was roughly flat in 1982, although the raw residual fell in both years.

(b) Output in equation (2) is value added: what capital and labor produce beyond the materials and energy that firms buy. When oil costs nearly four times as much, firms use less of it and switch to slower, less energy-intensive ways of working, and some energy-hungry machines stop paying their way. The same \(K\) and \(L\) then produce less value added, which the model reads as a lower \(\bar{A}\). With flexible prices, labor demand shifts down: employment and the real wage both fall, the first row of the table. This is the real business cycle story, and in 1974 it fits: true TFP fell by about 1.1 percent in a recession year.

(c) A monetary contraction with sticky prices: the output demanded falls, the vertical labor demand curve of equation (9) shifts left, and employment and the real wage both fall (the “monetary expansion, sticky prices” row, in reverse). The sticky-wage row, run in reverse, fits as well: with nominal wages stuck, a lower price level raises the real wage \(\bar{W}/P\), and firms hire fewer workers. Either way a nominal rigidity lets money cut employment, and TFP plays no role. The raw residual fell by 2.76 percent only because utilization fell by 2.94 percent: firms kept workers and machines on hand but ran them less intensively, and equation (3) counts that as lost productivity.

(d) The evidence is mixed. Some recessions, like 1974, come with a true fall in TFP; others, like 1982, have flat TFP and a clear monetary cause. Measured with the raw residual, both years look like TFP recessions, which is why the first RBC models looked so successful. A model of the cycle needs both TFP shocks and monetary shocks with nominal rigidities: the two flavors that modern DSGE models combine.

Problem 3 (Challenge): How elastic must labor supply be? Economists measure how strongly hours respond to a temporary change in the wage, holding consumption fixed, with the Frisch elasticity \(\eta\). Chetty, Guren, Manoli and Weber (NBER Working Paper 16729) note that business-cycle models need much larger labor supply elasticities than studies of individual workers find; their synthesis of the micro evidence points to Frisch elasticities of about 0.5 for the hours of people who work and 0.25 for the decision to work at all.

  1. Show that the labor supply curve (6) has a Frisch elasticity of 1.

  2. Replace the disutility of work in the household’s utility by \(\frac{\eta}{\eta+1}\,\bar{\ell}^{-1/\eta}L^{(\eta+1)/\eta}\). Derive the new labor supply curve and check that it is equation (6) when \(\eta = 1\). Then solve the labor market with labor demand (5) and no tax, and show that the elasticity of \(L^*\) with respect to \(\bar{A}\) is \(3\eta/(3+\eta)\).

  3. In Fernald’s data for 1950–2025, the growth of hours worked has a standard deviation of 2.57 percentage points and the growth of utilization-adjusted TFP one of 1.40 points (files tfp_inputs.csv and tfp_annual.csv, from the Federal Reserve Bank of San Francisco). If TFP shocks in the flexible-price labor market were the only reason hours move, how large would \(\eta\) have to be? Compare it with the micro estimate of 0.5 and with the chapter’s \(\eta = 1\).

Hint
  1. Take logs of equation (6) and change \(\ln w\) with \(c\) fixed. (b) The derivative of the new disutility with respect to \(L\) is \((L/\bar{\ell})^{1/\eta}\); set it equal to \(w/c\). Then repeat steps 1–4 of the labor market’s “Solving the model”, with the power \(1/\eta\) in place of 1. (c) If \(\ln L\) always moves \(\epsilon\) times as much as \(\ln\bar{A}\), its standard deviation is \(\epsilon\) times as large. Solve \(3\eta/(3+\eta) = \epsilon\) for \(\eta\).

Your answer:

# Your answer here
Solution

(a) Taking logs of (6), \(\ln L^s = \ln\bar{\ell} + \ln w - \ln c\). With \(c\) fixed, a 1 percent higher wage raises \(\ln L^s\) by 1 percent: \(\partial\ln L^s/\partial\ln w = 1\), so the Frisch elasticity is 1.

(b) Utility is now \(U = \ln c - \frac{\eta}{\eta+1}\bar{\ell}^{-1/\eta}L^{(\eta+1)/\eta}\), with \(c = wL - \bar{S}\). The power rule brings down \((\eta+1)/\eta\), which cancels the factor in front, and lowers the power by one, to \(1/\eta\):

\[\frac{dU}{dL} = \frac{w}{c} - \bar{\ell}^{-1/\eta}L^{1/\eta} = \frac{w}{c} - \left(\frac{L}{\bar{\ell}}\right)^{1/\eta} = 0 \quad\Longrightarrow\quad L^s = \bar{\ell}\left(\frac{w}{c}\right)^{\eta}.\]

With \(\eta = 1\) the disutility is \(\frac{1}{2}\bar{\ell}^{-1}L^2\) and \(L^s = \bar{\ell}\,w/c\): equation (6). Written as a wage, supply is \(w = c\,(L/\bar{\ell})^{1/\eta}\). Set it equal to labor demand (5):

\[c\,\bar{\ell}^{-1/\eta}L^{1/\eta} = \frac{2}{3}\cdot\frac{\bar{A}K^{1/3}}{L^{1/3}} \quad\Longrightarrow\quad L^{1/\eta + 1/3} = \frac{2}{3}\cdot\frac{\bar{A}K^{1/3}\,\bar{\ell}^{1/\eta}}{c}.\]

The power on the left is \(\frac{1}{\eta} + \frac{1}{3} = \frac{3+\eta}{3\eta}\), so raise both sides to \(\frac{3\eta}{3+\eta}\):

\[L^* = \left[\frac{2}{3}\cdot\frac{\bar{A}K^{1/3}\,\bar{\ell}^{1/\eta}}{c}\right]^{3\eta/(3+\eta)}.\]

In logs, \(\ln L^* = \text{const} + \frac{3\eta}{3+\eta}\ln\bar{A}\): the elasticity is \(3\eta/(3+\eta)\), which is \(3/4\) for \(\eta = 1\), as in the comparative-statics table. It is 0.429 for \(\eta = 0.5\) and 1.5 for \(\eta = 3\), and it can never reach 3, however elastic labor supply is: the falling marginal product caps it.

(c) The required elasticity is \(\epsilon = 2.57/1.40 = 1.84\). Solving \(3\eta/(3+\eta) = \epsilon\) gives \(3\eta = \epsilon(3+\eta)\), so \(\eta = 3\epsilon/(3-\epsilon) = 4.7\). That is nearly ten times the micro estimate of 0.5. With \(\eta = 0.5\), TFP shocks would move hours with a standard deviation of only \(0.429 \times 1.40 = 0.60\) points, under a quarter of the 2.57 in the data; with the chapter’s \(\eta = 1\), about \(0.75 \times 1.40 = 1.05\) points. Problem 4 adds a second difficulty: hours and adjusted TFP do not even move in the same direction.

inputs = pd.read_csv(DATA_URL + "tfp_inputs.csv")
ratio = inputs.hours_growth.std() / inputs.tfp_util.std()
print(f"standard deviations: hours {inputs.hours_growth.std():.2f}, adjusted TFP {inputs.tfp_util.std():.2f}")
print(f"elasticity needed {ratio:.2f}, so eta = {3 * ratio / (3 - ratio):.1f}")
for eta in (0.5, 1.0, 3.0):
    print(f"eta = {eta}: employment elasticity {3 * eta / (3 + eta):.3f}")

This is the labor-supply puzzle of real business cycle models: to let TFP shocks drive hours, they need labor supply far more elastic than studies of workers find. DSGE models answer with other shocks and with nominal rigidities.

Data-based problems

Problem 4 (Basic): Do hours rise when TFP rises? The file tfp_inputs.csv holds John Fernald’s growth-accounting data for the U.S. business sector, 1950–2025 (Federal Reserve Bank of San Francisco; see the data notes): the growth of output, hours, capital and labor quality, capital’s share, and both TFP measures with utilization. Compute the correlation of hours growth with the raw Solow residual and with utilization-adjusted TFP growth. The flexible-price labor market predicts that hours rise with TFP (an elasticity of 0.75); with sticky prices and unchanged demand, equation (9) predicts that they fall (an elasticity of −3/2). Which prediction do the numbers support, and why do the two TFP measures disagree?

Hint

inputs.hours_growth.corr(inputs.tfp_util) gives one correlation. To see why the measures disagree, also correlate hours with utilization, and remember equation (4).

# Your answer here

Your answer:

Solution

Read the file and compute three correlations:

inputs = pd.read_csv(DATA_URL + "tfp_inputs.csv")
print(f"hours and raw TFP:      {inputs.hours_growth.corr(inputs.tfp_solow):+.2f}")
print(f"hours and adjusted TFP: {inputs.hours_growth.corr(inputs.tfp_util):+.2f}")
print(f"hours and utilization:  {inputs.hours_growth.corr(inputs.utilization):+.2f}")

Hours growth has a correlation of +0.38 with the raw residual but −0.39 with adjusted TFP. Once utilization is removed, years of fast TFP growth tend to be years in which hours fall. That is the sign that sticky prices predict: when TFP rises and demand does not keep up, firms need fewer workers. Basu, Fernald and Kimball (2006), “Are Technology Improvements Contractionary?”, reach the same answer with careful estimates: inputs fall at first when technology improves, which fits simple sticky-price models but not the standard RBC model.

The raw residual disagrees because it contains utilization (equation 4), and utilization rises and falls with hours: their correlation is +0.65. Firms that hire more also run their workers and machines harder, and plain growth accounting records the extra intensity as higher TFP.

Problem 5 (Intermediate): Growth accounting by hand. Fernald computes the raw Solow residual (tfp_solow) from the columns of tfp_inputs.csv. Rebuild it yourself.

  1. Use equation (3), with the weights 1/3 and 2/3, to compute the residual in every year from output, capital and hours growth; the notebook’s solow_residual function works on whole columns. Compare your residual with tfp_solow in 2009 and 2020.

  2. Over 1950–2025, what are the correlation and the average gap between your residual and Fernald’s?

  3. Fernald’s formula differs from equation (3) in two ways: capital’s share \(\alpha\) is measured every year (capital_share), and labor input also grows with labor quality \(g_q\) (labor_quality), the skill and experience mix of the hours worked: \(g_A = g_Y - \alpha g_K - (1-\alpha)(g_L + g_q)\). Recompute the residual with this formula. How large is the largest gap now?

  4. In which two years did labor quality grow fastest, and why might it jump in those years?

Hint

solow_residual(inputs.gdp_growth, inputs.capital_growth, inputs.hours_growth) returns a whole column; store it as a new column. inputs.year.isin([2009, 2020]) picks the two years, and .nlargest(3, "labor_quality") the years with the fastest labor-quality growth. Look at hours growth in those years.

# Your answer here
Solution
inputs = pd.read_csv(DATA_URL + "tfp_inputs.csv")
inputs["simple"] = solow_residual(inputs.gdp_growth, inputs.capital_growth, inputs.hours_growth)
alpha = inputs.capital_share
inputs["full"] = (inputs.gdp_growth - alpha * inputs.capital_growth
                  - (1 - alpha) * (inputs.hours_growth + inputs.labor_quality))
print(inputs[inputs.year.isin([2009, 2020])][["year", "simple", "full", "tfp_solow"]].round(2).to_string(index=False))
gap = inputs.simple - inputs.tfp_solow
print(f"equation (3): correlation {inputs.simple.corr(inputs.tfp_solow):.3f}, average gap {gap.mean():.2f}")
print(f"with alpha and labor quality: largest gap {(inputs.full - inputs.tfp_solow).abs().max():.2f}")
print(inputs.nlargest(3, "labor_quality")[["year", "hours_growth", "labor_quality"]].to_string(index=False))

(a) In 2009, equation (3) gives \(-3.75 - \tfrac{1}{3}\times 1.76 - \tfrac{2}{3}\times(-7.74) = +0.82\) percent with these rounded numbers (+0.83 from the unrounded data), against Fernald’s −0.15; in 2020 it gives +1.27 against −0.23. In both deep recessions the simple formula even turns the sign.

(b) Over all 76 years the two residuals have a correlation of 0.988, but the simple one is on average 0.23 points higher.

(c) With the measured capital share and labor quality, the residual is −0.25 in 2009 and −0.29 in 2020, and the largest gap in any year is 0.10 points; it is not zero only because the identity is exact for quarterly data and approximate for annual averages. Most of the average gap of (b) is labor quality: it grows by 0.38 percent a year on average, and equation (3) leaves its contribution, \(\tfrac{2}{3} \times 0.38 = 0.25\), inside TFP.

(d) The fastest years are 2020 (1.63 percent) and 2009 (1.19 percent), the two years in which hours fell by about 7.7 percent. Labor quality weights each hour by what that kind of worker earns. When it jumps, the hours of less experienced and lower-paid workers have fallen much more than the others, so the average quality of the hours still worked rises. Without that correction, equation (3) counts the better mix of workers as higher TFP.

Problem 6 (Challenge): Is the United States an exception? In Fig. 1’s “Beyond the United States” chart, the Penn World Table 11.0 TFP of the United States fell in only half of its years of falling GDP, while most countries’ TFP fell almost every time.

  1. For each of the 120 countries in pwt_growth.csv, compute the correlation between GDP growth and TFP growth over all of its years, and collect the results in a table.

  2. What is the median correlation? Where does the United States rank, counting from the lowest? Which five countries have the lowest correlations?

  3. Compute the median correlation in each region. Which region stands out?

  4. Does a high correlation show that TFP shocks cause booms and recessions? Use equation (4) and Problem 4.

Hint

Loop over sorted(pwt.country.unique()), keep one country’s rows, and compute .corr() of its two columns; append a small dictionary per country to a list and turn the list into a DataFrame with pd.DataFrame(rows). Then .sort_values("r"), .median() and .groupby("region").

# Your answer here

Your answer:

Solution
pwt = pd.read_csv(DATA_URL + "pwt_growth.csv")
rows = []
for country in sorted(pwt.country.unique()):
    data = pwt[pwt.country == country]
    rows.append({"country": country, "region": data.region.iloc[0], "years": len(data),
                 "r": data.gdp_growth.corr(data.tfp_growth)})
table = pd.DataFrame(rows).sort_values("r")
us_r = table[table.country == "United States"].r.iloc[0]
print(f"median correlation {table.r.median():.2f}; United States {us_r:.2f}, "
      f"rank {(table.r < us_r).sum() + 1} of {len(table)} from the lowest")
print(table.head(5).round(2).to_string(index=False))
print(table.groupby("region").r.median().round(2).sort_values())

rows.append({...}) adds one dictionary, one row of the future table, to the list. .iloc[0] takes the first value of a column.

(b) The median correlation is 0.86: in most countries measured TFP rises and falls almost one for one with GDP. The United States, at 0.70, ranks 15th of 120 from the lowest. The five lowest are Serbia (0.18, with only 29 years of data), Tanzania (0.45), Colombia (0.51), Canada (0.56) and South Africa (0.59).

(c) North America, that is Canada and the United States, has a median of 0.63; every other region lies between 0.83 and 0.91. So the United States is unusual, but not alone: its neighbor is even lower.

(d) No. PWT’s TFP is a raw Solow residual, and by equation (4) it contains the change in utilization. Whenever GDP falls for any reason (less demand, tighter credit, a monetary contraction), firms use their workers and machines less intensively, and the residual falls with GDP. The correlation can therefore run from GDP to measured TFP, not from TFP to GDP. For the United States, the one country here with a utilization-adjusted series, Problem 4 found that adjusted TFP and hours move in opposite directions.

Modeling problems

Problem 7 (Basic): Could a fall in TFP explain 2020? In 2020 U.S. business output fell by 2.90 percent, while Fernald’s utilization-adjusted TFP rose by 1.81 percent (tfp_annual.csv). Feed this measured shock into the flexible-price labor market: move the slider from the baseline \(\bar{A} = 1\) to the 2020 level \(\bar{A}' = 1.0181\), as close as the slider allows, and read employment, the real wage and output in the title. Does the model predict a recession or a boom? By how many percentage points does its change in output miss the measured −2.90 percent, and what does that say about the shocks behind the 2020 recession?

Python note. fixed(C_BAR) holds an argument at one value, so interact draws no slider for it.

Hint

At the baseline, TFP and output are both 1, so an output of 1.05 in the title would mean 5 percent more output than before the shock. Compare the sign of the model’s change with the sign in the data first, then the size. The title rounds to two decimals; for more, use equilibrium(A=...) and output(L, A).

interact(labor_market,
         A=FloatSlider(value=1.0, min=0.9, max=1.1, step=0.005, readout_format=".3f", description="TFP Ā",
                       style=WIDE),
         c=fixed(C_BAR), ell=fixed(ELL_BAR));

Source. The model of Fig. 2 above, adapted from Fig. 15.2 in Jones (2024), Macroeconomics, 6th ed.

# Your answer here
Solution

The closest slider value is \(\bar{A} = 1.020\). The title reads employment 1.01, a real wage of 0.68 and output 1.03: the model predicts a boom, with more jobs, higher wages and more output. The functions give the exact numbers:

L_2020, w_2020 = equilibrium(A=1.0181)       # the measured rise of 1.81 percent
Y_2020 = output(L_2020, 1.0181)
print(f"TFP 1.0181: employment {L_2020:.4f}, real wage {w_2020:.4f}, output {Y_2020:.4f}")
print(f"Output: model {100 * (Y_2020 - 1):+.2f}%, data -2.90%, a miss of {100 * (Y_2020 - 1) + 2.90:.2f} points")

With \(\bar{A}' = 1.0181\), employment is 1.0135, the real wage 0.6757 and output 1.0273: output rises by 2.73 percent, about 3/2 times the rise in TFP. Measured output fell by 2.90 percent, so the model misses by 5.63 percentage points, and in the wrong direction. A TFP shock cannot explain the pandemic recession: the fall in output must have come from other shocks, to the supply of labor and to spending.

Problem 8 (Intermediate): How large is the spending multiplier with flexible prices? A government raises its purchases by \(\Delta G\), paid for with taxes. Households, poorer over their lifetimes, cut consumption, and as in Fig. 5 they work more. The multiplier \(\Delta Y/\Delta G\) is the rise in output per unit of extra purchases. The sliders below set \(\Delta G\) and the share of it that households cut from consumption: 1 for a permanent rise, less for a temporary one.

  1. For a permanent rise of \(\Delta G = 0.1\), what are employment, output and the multiplier?

  2. Keep \(\Delta G = 0.1\). What is the smallest share on the slider that gives a multiplier of at least 0.8? Then find the exact share with the output formula of (a). Is a program with that share closer to a permanent or to a temporary rise in purchases?

  3. Ramey (2011) concludes that the multiplier for a temporary, deficit-financed rise in purchases is probably between 0.8 and 1.5 (Journal of Economic Literature). Can the flexible-price model deliver that? What would you add to it?

Python note. The widget cell defines a small function that sets consumption from the two sliders, redraws Fig. 2’s diagram with labor_market, and then replaces its title with fig.update_layout(title=...).

Hint

Consumption is \(c' = 0.6 - \text{share} \times \Delta G\). Output changes only through employment, because \(\bar{A}\) and \(K\) do not move. For (b), raise the share one step at a time and watch the multiplier in the title. For the exact share, work backward: first the output that a multiplier of 0.8 requires, then the consumption that gives that output.

def spending_multiplier(G_rise=0.1, share=1.0):
    """Figs. 2 and 5 again: purchases rise by G_rise and consumption falls by share x G_rise."""
    c_new = C_BAR - share * G_rise
    L_base, w_base = equilibrium()
    L_new, w_new = equilibrium(c=c_new)
    multiplier = (output(L_new) - output(L_base)) / G_rise
    fig = labor_market(c=c_new)
    fig.update_layout(title=f"Employment {L_new:.2f}, output {output(L_new):.2f}, "
                            f"multiplier ΔY/ΔG = {round(multiplier, 2) + 0.0:.2f}")
    return fig


interact(spending_multiplier,
         G_rise=FloatSlider(value=0.1, min=0.02, max=0.2, step=0.02, description="rise in purchases ΔG", style=WIDE),
         share=FloatSlider(value=1.0, min=0.0, max=1.0, step=0.1, description="share cut from consumption",
                           style=WIDE));

Source. The model of Figs. 2 and 5 above, adapted from Figs. 15.2 and 15.5 in Jones (2024), Macroeconomics, 6th ed.

# Your answer here

Your answer:

Solution
L_base, w_base = equilibrium()
for share in (1.0, 0.8, 0.9, 0.3):
    L_new, w_new = equilibrium(c=C_BAR - share * 0.1)
    multiplier = (output(L_new) - output(L_base)) / 0.1
    print(f"share {share}: employment {L_new:.3f}, output {output(L_new):.3f}, "
          f"multiplier {round(multiplier, 2) + 0.0:.2f}")
c_exact = C_BAR / 1.08 ** 2                  # the consumption that gives output 1.08
print(f"A multiplier of 0.8 needs c' = {c_exact:.3f}, a share of {(C_BAR - c_exact) / 0.1:.2f}")

(a) A permanent rise cuts consumption from 0.6 to 0.5. By equation (7), employment rises to \((0.6/0.5)^{3/4} = 1.147\), and output to \((0.6/0.5)^{1/2} = 1.095\). The multiplier is \((1.095 - 1)/0.1 = 0.95\).

(b) On the slider, a share of 0.8 gives a multiplier of 0.74 and a share of 0.9 gives 0.85, so 0.9 is the smallest share that reaches 0.8. Exactly: a multiplier of 0.8 needs output \(1 + 0.8 \times 0.1 = 1.08\). By (a), output is \((0.6/c')^{1/2}\), so \(c' = 0.6/1.08^2 = 0.514\), and the share is \((0.6 - 0.514)/0.1 = 0.86\). Households must cut their consumption by 86 percent of the extra purchases, almost as much as for a permanent rise. With flexible prices, extra purchases raise output only through the wealth effect: people work more because they feel poorer.

(c) No. For a temporary rise, the case Ramey studies, households lose little lifetime income, so they cut consumption by much less than the 86 percent of (b), and the flexible-price multiplier stays far below 0.8: with a share of 0.3 it is only 0.26, and less the shorter the program. Only a permanent, tax-financed rise gets close to 1, and then consumption and the real wage fall. To reach a multiplier near 1 for a temporary program, add sticky prices: firms then produce whatever is demanded, so the extra purchases raise output directly, as in Fig. 8. The estimated Smets–Wouters model, which has sticky prices, finds a multiplier of about 1 (the note after Fig. 5).

Problem 9 (Intermediate): One employment target, two frictions. The central bank wants a monetary expansion that raises employment by 10 percent. How large must the expansion be, and what happens to the real wage,

  1. when nominal wages are sticky at \(\bar{W} = 0.75\), as in Figs. 6–7, starting from point \(A\) with employment 0.70 (the expansion raises the price level by size percent);

  2. when prices are sticky, as in Fig. 8, starting from \(Y_0 = 1\) (the expansion raises the output demanded by size percent)?

  3. Which case is better for the workers who already have a job, and which looks more like an actual boom?

Get close with the sliders, then find the exact answers with the model’s equations.

Hint

With sticky wages, firms are on their labor demand curve, and employment grows with the cube of the price level (the Model overview’s sticky-wage block). With sticky prices, use equation (9) with \(\bar{A} = K = 1\). Set each employment ratio equal to 1.1 and solve for \(P'\) and \(Y'\).

def expansion(friction="sticky wages", size=3.0):
    """Figs. 7 and 8 again: prices (sticky wages) or the output demanded (sticky prices) rise by size percent."""
    if friction == "sticky wages":
        return sticky_wages(t=1, P_new=1 + size / 100, W_bar=0.75)
    fig = sticky_prices(Y_new=1 + size / 100, c_new=C_BAR)
    for trace in fig.data:
        if trace.text == ("A",):
            trace.textposition = "top left"      # keeps the letter A clear of a nearby point B
    return fig


interact(expansion,
         friction=ToggleButtons(options=["sticky wages", "sticky prices"], value="sticky wages",
                                description="friction"),
         size=FloatSlider(value=3.0, min=2.0, max=10.0, step=0.5, readout_format=".1f",
                          description="size of the expansion (percent)", style=WIDE));

Source. The models of Figs. 7 and 8 above, adapted from Figs. 15.7 and 15.8 in Jones (2024), Macroeconomics, 6th ed.

# Your answer here

Your answer:

Solution

(a) Along labor demand, \(L^d = \left[\tfrac{2}{3}\big/(\bar{W}/P')\right]^3 = (P')^3 L^d(\bar{W})\), so employment rises by 10 percent when \((P')^3 = 1.1\), that is \(P' = 1.1^{1/3} = 1.032\): prices must rise by about 3.2 percent. On the slider, 3 percent gives employment 0.77. The real wage falls from 0.75 to \(0.75/1.032 = 0.727\), by 3.1 percent.

(b) By equation (9), \(L = (Y')^{3/2}\), so \((Y')^{3/2} = 1.1\) gives \(Y' = 1.1^{2/3} = 1.066\): the output demanded must rise by 6.6 percent (on the slider, 6.5 percent already gives employment 1.10). The real wage lies on the supply curve, \(w = cL/\bar{\ell}\), so it rises by the same 10 percent, from 0.667 to 0.733.

P_new = 1.1 ** (1 / 3)
L_A, L_B = labor_demand(0.75), labor_demand(0.75 / P_new)
print(f"Sticky wages: P' = {P_new:.4f}, employment {L_A:.3f} -> {L_B:.3f}, real wage 0.750 -> {0.75 / P_new:.3f}")
Y_new = 1.1 ** (2 / 3)
L_sticky = labor_needed(Y_new)
print(f"Sticky prices: Y' = {Y_new:.4f}, employment 1.000 -> {L_sticky:.3f}, "
      f"real wage {supply_wage(1.0):.3f} -> {supply_wage(L_sticky):.3f}")

(c) With sticky wages the new jobs are paid for by everyone who already works: their real wage falls by 3.1 percent. With sticky prices employment and the real wage rise together, by 10 percent each, as they tend to do in actual booms. The two pushes differ in size for two reasons. Prices need to rise by only 3.2 percent because labor demand is very elastic: firms hire about 3 percent more for each 1 percent fall in the real wage. Output demanded must rise by 6.6 percent, two thirds of the employment gain, because \(Y = L^{2/3}\): each extra worker adds less output than the last.

Problem 10 (Challenge): A TFP shock or a tax-like wedge in 2009? In 2009, hours worked in the U.S. business sector fell by 7.74 percent and output by 3.75 percent, while utilization-adjusted TFP rose by 3.53 percent (tfp_inputs.csv). Chari, Kehoe and McGrattan (2007) showed that many frictions act in a growth model like time-varying wedges that resemble taxes on labor or investment (Federal Reserve Bank of Minneapolis staff report; published in Econometrica, doi:10.1111/j.1468-0262.2007.00768.x). Jermann and Quadrini (2012) find that tighter financing conditions for firms contributed to the 2008–2009 recession (American Economic Review). In our labor market, the sales tax \(\tau\) is such a wedge.

  1. With the sliders, check that a TFP fall to \(\bar{A}' = 0.90\) with no wedge, and a wedge \(\tau = 0.10\) with TFP unchanged, both lower employment to 0.92, close to the fall in hours of 2009.

  2. For each story, compute the change in output and the measured Solow residual of equation (3), with capital fixed, in percent (100 times the change in the log, as in Fernald’s data).

  3. Which story fits the 2009 data better?

  4. Suppose the wedge comes from expensive credit: firms must borrow to pay wages before they sell their output. Which policies would raise employment in that case, and why would they do little against a true fall in TFP?

Hint

Only the product \((1-\tau)\bar{A}\) enters equation (7), but output \(Y = \bar{A}K^{1/3}L^{2/3}\) contains \(\bar{A}\) alone. For (b), use equilibrium, output, np.log and solow_residual(g_Y, 0.0, g_L).

interact(labor_shock,
         A_new=FloatSlider(value=1.0, min=0.8, max=1.0, step=0.1, description="TFP after the shock Ā′", style=WIDE),
         tau=FloatSlider(value=0.0, min=0.0, max=0.2, step=0.1, description="tax-like wedge τ", style=WIDE),
         c_new=fixed(C_BAR));

Source. The model of Figs. 3 and 4 above, adapted from Figs. 15.3 and 15.4 in Jones (2024), Macroeconomics, 6th ed.

# Your answer here

Your answer:

Solution

(a) In both cases \((1-\tau)\bar{A} = 0.9\), so equation (7) gives the same point: employment \(0.9^{3/4} = 0.924\) and a real wage of 0.616. The title reads “Employment 1.00 → 0.92, real wage 0.67 → 0.62” for either slider.

(b) Output and the residual differ, because only the TFP story lowers \(\bar{A}\) in the production function:

for A_new, tau in [(0.9, 0.0), (1.0, 0.1)]:
    L_new, w_new = equilibrium(A=A_new, tau=tau)
    g_L = 100 * np.log(L_new)                     # percent, as 100 times the change in the log
    g_Y = 100 * np.log(output(L_new, A_new))
    residual = round(solow_residual(g_Y, 0.0, g_L), 2) + 0.0
    print(f"A = {A_new}, tau = {tau}: employment {g_L:.2f}%, output {g_Y:.2f}%, Solow residual {residual:.2f}%")
Story Employment, percent Output, percent Solow residual, percent
TFP falls to 0.90 −7.90 −15.80 −10.54
wedge \(\tau = 0.10\) −7.90 −5.27 0.00

In the TFP story, \(g_Y = g_A + \tfrac{2}{3}g_L = -10.54 + \tfrac{2}{3}\times(-7.90) \approx -15.8\). In the wedge story, \(\bar{A}\) does not change, so \(g_Y = \tfrac{2}{3}g_L = -5.27\) and the residual is zero.

(c) The wedge story. With hours down by about 8 percent, a TFP recession would have cut output by about 16 percent and measured TFP by more than 10 percent. In 2009 output fell by 3.75 percent, the raw residual by only 0.15 percent, and adjusted TFP rose. The wedge story gets both facts roughly right: an output fall of about 5 percent (somewhat less in the data, because capital still grew by 1.76 percent) and no fall in TFP.

(d) If the wedge is expensive credit, policies that make borrowing cheaper or easier lower \(\tau\) and shift labor demand back up: lower interest rates, central bank lending to banks and firms, loan guarantees, or rebuilding banks’ capital. Against a true TFP fall they would do little, because each worker really produces less: cheaper credit does not raise the marginal product of labor, so firms would still want fewer workers at any given wage.