#### Plot investment function
# Set parameter values
alpha=1.2 # adjustment speed of output
delta=0.2 # depreciation rate
sigma=0.4 # propensity to save
Y_E=10 # normal level of output
gamma=0.6 # sensitivity of investment to deviations of actual from normal cap stock
K=sigma*Y_E/delta # set capital stock to normal level
# Create investment function using Y as argument
inv= function(Y){
sigma*Y_E + gamma*(sigma*Y_E/delta - K) + atan(Y - Y_E)
}
# Plot the function in (I,Y) space
curve(inv, from = 5, to = 15, col = 1, xlab="Y", ylab="I" , main="",
lwd=1.5, n=10000, ylim=range(2,6))
abline(v=Y_E, col=2)
legend("bottomright", legend = c("I", expression(Y^E)),
col = c(1, 2), lwd = 2, bty = "n")
18 A Kaldorian Endogenous Business Cycle Model
Overview
This model captures some key features of Nicholas Kaldor (1940)’s nonlinear model of endogenous business cycle fluctuations. Unlike Samuelson (1939)’s linear multiplier-accelerator model (see Chapter 2 and Chapter 9), which usually requires repeated external shocks to produce sustained cycles, Kaldor (1940) outlined a model that would produces endogenous, i.e. shock-independent, fluctuations.1 Kaldor (1940) assumed that investment and saving were very sensitive to income for normal levels of income, but relatively insensitive for extreme values of income. Graphically, these assumptions give rise to sigmoid-shaped investment and saving functions. As a result, the goods market equilibrium becomes locally unstable due to strong positive feedback effects, but then becomes stable once the economy has sufficiently moved away from equilibrium during booms or busts. Similar to Hicks (1950) (see Chapter 17), Kaldor (1940) also assumed a locally unstable goods market, but unlike Hicks who postulated discrete bounds that would prevent fluctuations from exploding, Kaldor (1940) considered smooth bounds that stemmed from the investment and saving behaviour of firms and households. For instance, at high levels of output, investment may become insensitive to output due to rising costs of construction or surging financing cost. Similarly, for low levels of output profit opportunities may be missing, thereby rending investment insensitive to changes in output. It is then the interaction between output and the capital stock that turns these bounded output dynamics into a cycle: Kaldor (1940) assumed that the capital stock exerts a negative effect on investment as an increasing capital stock lowers the marginal efficiency of capital, whereas output pushes up the dynamics of the capital stock due to the accelerator effect.
Kaldor (1940)’s initial paper mostly relied on graphical analysis. A continuous-time version formalisation of his graphical was subsequently presented in Chang and Smyth (1971). Textbook treatments can be found in Gabisch and Lorenz (1989), chapter 4. We present a discrete time version of the Kaldor model due to Bischi et al. (2001).
The Model
where
Equation 18.1 specifies that output reacts slugglishly to excess demand
#### Plot investment function
# Import necessary libraries
import numpy as np
import matplotlib.pyplot as plt
# Set parameter values
= 1.2 # adjustment speed of output
alpha = 0.2 # depreciation rate
delta = 0.4 # propensity to save
sigma = 10 # normal level of output
Y_E = 0.6 # sensitivity of investment to deviations of actual from normal cap stock
gamma = sigma * Y_E / delta # set capital stock to normal level
K
# Define the investment function using Y as argument
def inv(Y):
return sigma * Y_E + gamma * (sigma * Y_E / delta - K) + np.arctan(Y - Y_E)
# Plot the function in (I,Y) space
= np.linspace(5, 15, 10000)
Y_values = inv(Y_values)
I_values
=(8,6))
plt.figure(figsize='black', linewidth=1.5, label='I')
plt.plot(Y_values, I_values, color=Y_E, color='red', linestyle='--', label='$Y^E$')
plt.axvline(x
# Customize the plot
"Y")
plt.xlabel("I")
plt.ylabel("Investment Function")
plt.title(2, 6)
plt.ylim(="lower right", frameon=False)
plt.legend(locTrue, linestyle='--', alpha=0.6)
plt.grid(
plt.show()
Simulation
Parameterisation
Table 1 reports the parameterisation used in the simulation. We will analyse below how the model’s dynamic properties change as
Table 1: Parameterisation
1.2 | 0.2 | 0.4 | 10 | 0.6 |
Simulation code
# Clear the environment
rm(list=ls(all=TRUE))
# Set number of periods
Q=200
# Set number of scenarios
Scen=1
# Create (Scen x Q)-matrices that will contain the simulated data
Y=matrix(data=1,nrow=Scen,ncol=Q) # Income/output
K=matrix(data=1,nrow=Scen,ncol=Q) # capital stock
S=matrix(data=1,nrow=Scen,ncol=Q) # saving
I=matrix(data=1,nrow=Scen,ncol=Q) # Investment
# Set fixed parameter values
alpha=1.2 # adjustment speed of output
delta=0.2 # depreciation rate
sigma=0.4 # propensity to save
Y_E=10 # normal level of output
gamma=0.6 # sensitivity of investment to deviations of actual from normal cap stock
# Simulate the model by looping over Q time periods for Scen different scenarios
for (i in 1:Scen){
for (t in 2:Q){
for (iterations in 1:500){ # run the model 500-times in each period
#Model equations
#(1) Output
Y[i,t] = Y[i,t-1] + alpha*(I[i,t-1] - S[i,t-1])
#(2) Capital stock
K[i,t] = (1-delta)*K[i,t-1] + I[i,t-1]
#(3) Saving
S[i,t] = sigma*Y[i,t]
#(4) Investment
I[i,t] = sigma*Y_E + gamma*(sigma*Y_E/delta - K[i,t]) + atan(Y[i,t] - Y_E)
} # close iterations loop
} # close time loop
} # close scenario loop
# Import necessary libraries
import numpy as np
# Set number of periods
= 200
Q
# Set number of scenarios
= 1
Scen
# Create (Scen x Q)-matrices to contain the simulated data
= np.ones((Scen, Q)) # Income/output
Y = np.ones((Scen, Q)) # Capital stock
K = np.ones((Scen, Q)) # Saving
S = np.ones((Scen, Q)) # Investment
I
# Set fixed parameter values
= 1.2 # Adjustment speed of output
alpha = 0.2 # Depreciation rate
delta = 0.4 # Propensity to save
sigma = 10 # Normal level of output
Y_E = 0.6 # Sensitivity of investment to deviations of actual from normal capital stock
gamma
# Simulate the model by looping over Q time periods for Scen different scenarios
for i in range(Scen):
for t in range(1, Q):
for iterations in range(500): # Run the model 500 times in each period
# Model equations
# (1) Output
= Y[i, t-1] + alpha * (I[i, t-1] - S[i, t-1])
Y[i, t]
# (2) Capital stock
= (1 - delta) * K[i, t-1] + I[i, t-1]
K[i, t]
# (3) Saving
= sigma * Y[i, t]
S[i, t]
# (4) Investment
= sigma * Y_E + gamma * (sigma * Y_E / delta - K[i, t]) + np.arctan(Y[i, t] - Y_E) I[i, t]
Plots
Figure 18.2 displays the dynamics of aggregate output. The model generates endogenous cycles in output that are permanent, i.e. they don’t require any external shocks, and they are (roughly) periodic with a cycle length from peak (trough) to peak (trough) of around 17 periods. What generates the turning points? Suppose income is close to its normal level of
# Set start and end periods for plots
Tmax=100
Tmin =10
# Plot aggregate output and capital stock
plot(Y[1, Tmin:Tmax], type="l", col=1, lwd=2, lty=1, xlab="Time", ylab="Y")
title(main="Aggregate output and capital stock", cex=0.8)
par(mar = c(5, 4, 4, 4) + 0.3)
par(new = TRUE)
plot(K[1, Tmin:Tmax], type="l", col=1, lwd=2, lty=2, font.main=1, cex.main=1,ylab = '', axes=FALSE,
xlab = '', ylim = range(K[1, Tmin:Tmax]), cex=0.8)
axis(side = 4, at=pretty(K[1, Tmin:Tmax]))
mtext("K", side = 4, line = 3)
legend("topright", legend=c("Y", "K"),
lty=1:2, cex=0.8, bty = "n", y.intersp=0.8)
# Set start and end periods for plots
= 100
Tmax = 10
Tmin
# Plot aggregate output and capital stock
= plt.subplots(figsize=(8, 6))
fig, ax1
# Plot Y (aggregate output)
range(Tmin, Tmax), Y[0, Tmin:Tmax], color='black', linewidth=2, linestyle='-', label='Y')
ax1.plot("Time")
ax1.set_xlabel("Y", color='black')
ax1.set_ylabel(='y', labelcolor='black')
ax1.tick_params(axis"Aggregate Output and Capital Stock", fontsize=10)
ax1.set_title(
# Create a twin axis for the capital stock
= ax1.twinx()
ax2
# Plot K (capital stock)
range(Tmin, Tmax), K[0, Tmin:Tmax], color='black', linewidth=2, linestyle='--', label='K')
ax2.plot("K", color='black')
ax2.set_ylabel(='y', labelcolor='black') ax2.tick_params(axis
Figure 18.3 displays the endogenous fluctuations in saving and investment. The horizontal lines represent the average level of saving and investment computed from the simulated data.2 It can be seen that these are virtually identical, reflecting the fact that that the model generates cycles around the goods market equilibrium but never reaches it.
## Calculate average saving and investment ignoring the first 10 periods
S_avr=rowMeans(S[,10:Q, drop=FALSE])
I_avr=rowMeans(I[,10:Q, drop=FALSE])
#Plot saving and investment along with their long-run average values
plot(S[1, Tmin:(Tmax)],type="l", col=1, lwd=2, lty=1, xlab="", ylab="S, I",
ylim=range(I[1, Tmin:Tmax],S[1, Tmin:(Tmax)]))
title(main="Saving and investment", xlab = 'Time',cex=0.8,line=2)
lines(I[1, Tmin:Tmax],lty=2)
abline(h=S_avr, col=2)
abline(h=I_avr, col=3)
legend("topleft", legend=c("S", "I"),
lty=1:2, cex=0.8, bty = "n", y.intersp=0.8)
Creating a bifurcation diagram
Next, we explore numerically under which conditions the model generates endogenous cycles. To do so, we need to introduce some terminology. First, a limit cycle is defined as a closed orbit of the state variables of a dynamic system around a locally unstable equilibrium (see Gandolfo (2009), chap. 23). In the neighbourhood of the equilibrium, the system is unstable and gets pushed away from it. However, rather than exhibiting explosive behaviour, the system eventually reaches a periodic cycle as it is bounded by nonlinearities. The fluctuations in Figure 18.2 indeed constitute a limit cycle. Second, a bifurcation is defined as a qualitative change of the behaviour of a dynamic system that occurs as a parameter of the system crosses a critical value (see Gandolfo (2009), chap. 24). Third, a Hopf bifurcation is a bifurcation that gives rise to a limit cycle.3
We can analyse the emergence of Hopf bifurcations in this model by means of so-called bifurcation diagrams that plot the dynamics of a representative endogenous variable variable from the model against different values of a parameter of interest. This is accomplished by fixing a parameterisation, simulating the model for a specific value of the parameter of interest, saving the last
The following code first creates a function called kaldor that simulates the Kaldor model,4 taking values of
# Clear the environment
rm(list=ls(all=TRUE))
# Define a function called "kaldor" that simulates a reduced-form version of the model
# and returns the last 50 values of Y; use alpha and sigma as arguments that
# need to be supplied when the function is called
kaldor <- function(alpha, sigma) {
# Set how many last values of output you want to save
T_0 = 50
# Set number of periods
Q = 200
# Create matrices for simulated data
Y = matrix(data = 1, nrow = 1, ncol = Q) # Income/output
K = matrix(data = 1, nrow = 1, ncol = Q) # Capital stock
# Set fixed parameter values
gamma = 0.6 # Sensitivity of investment to deviations of actual from normal cap stock
delta = 0.2 # Depreciation rate
Y_E = 10 # Normal level of output
# Simulate the model by looping over time periods
for (t in 2:Q) {
## Model equations
# Output
Y[1, t] = Y[1, t-1] +
alpha * (sigma * Y_E + gamma * (sigma * Y_E / delta - K[1, t-1]) +
atan(Y[1, t-1] - Y_E) - sigma * Y[1, t-1])
# Capital stock
K[1, t] = (1 - delta) * K[1, t-1] +
sigma * Y_E + gamma * (sigma * Y_E / delta - K[1, t-1]) +
atan(Y[1, t-1] - Y_E)
} # Close time loop
return(Y[1, (Q-T_0):Q]) # Return last 50 periods of output
}
Next, we prepare an initially empty bifurcation diagram and then loop over the kaldor function, increasing the parameter
# Prepare the bifurcation diagram in alpha
plot(NULL, xlim = c(0.5, 2.0), ylim = c(7.5, 12.5),
xlab = expression(alpha), ylab = expression(Y[t]),
pch = ".", cex = 0.6, col = "blue", main =
expression("Bifurcation diagram in " * alpha))
# Run kaldor function for different values of alpha (keeping sigma at 0.4)
# and place data points on bifurcation diagram
alpha = 0.5 # initialise alpha
while (alpha <= 2) { # run kaldor model until alpha assumes value of 2
output = kaldor(alpha=alpha, sigma=0.4) # obtain values of Y for given value of alpha
points(rep(alpha, length(output)), output, pch = ".", col = "blue", cex = 2) # add data points to diagram
alpha = alpha + 0.01 # increase alpha stepwise
}
### Generate kaldor function
# Define a function called "kaldor" that simulates a reduced-form version of the model
# and returns the last 50 values of Y
def kaldor(alpha, sigma):
# Set how many last values of output you want to save
= 50
T_0
# Set number of periods
= 200
Q
# Create matrices for simulated data
= np.ones((1, Q)) # Income/output
Y = np.ones((1, Q)) # Capital stock
K
# Set fixed parameter values
= 0.6 # Sensitivity of investment to deviations of actual from normal capital stock
gamma = 0.2 # Depreciation rate
delta = 10 # Normal level of output
Y_E
# Simulate the model by looping over time periods
for t in range(1, Q):
# Model equations
# (1) Output
0, t] = Y[0, t-1] + alpha * (
Y[* Y_E + gamma * (sigma * Y_E / delta - K[0, t-1]) +
sigma 0, t-1] - Y_E) - sigma * Y[0, t-1])
np.arctan(Y[
# (2) Capital stock
0, t] = (1 - delta) * K[0, t-1] + (
K[* Y_E + gamma * (sigma * Y_E / delta - K[0, t-1]) +
sigma 0, t-1] - Y_E))
np.arctan(Y[
# Return last 50 periods of output
return Y[0, (Q-T_0):Q]
#### Generate bifurcation diagram
# Prepare the bifurcation diagram in alpha
=(10, 6))
plt.figure(figsizer'Bifurcation Diagram in $\alpha$', fontsize=14)
plt.title(r'$\alpha$', fontsize=12)
plt.xlabel(r'$Y_t$', fontsize=12)
plt.ylabel(0.5, 2.0)
plt.xlim(7.5, 12.5)
plt.ylim(
# Run kaldor function for different values of alpha (keeping sigma at 0.4)
# and place data points on bifurcation diagram
= 0.5 # Initialize alpha
alpha while alpha <= 2.0:
# Obtain values of Y for the given value of alpha
= kaldor(alpha=alpha, sigma=0.4)
output
# Add data points to the diagram
* len(output), output, color='blue', s=4, marker='.')
plt.scatter([alpha]
# Increase alpha stepwise
+= 0.01
alpha
# Show the bifurcation diagram
plt.show()
Figure 18.5 does the same for the parameter
# Prepare the bifurcation diagram in sigma
plot(NULL, xlim = c(0.1, 0.5), ylim = c(7, 11.5),
xlab = expression(sigma), ylab = expression(Y[t]),
pch = ".", cex = 0.6, col = "blue",
main = expression("Bifurcation diagram in " * sigma))
# Run kaldor function for different values of sigma (keeping alpha at 1.2)
# and place data points on bifurcation diagram
sigma = 0.1 # initialise alpha
while (sigma <= 0.5) { # run kaldor model until sigma assumes value of 0.5
output = kaldor(alpha=1.2, sigma=sigma) # obtain the values of Y for given value of sigma
points(rep(sigma, length(output)), output, pch = ".", col = "blue", cex = 1) # add data points to diagram
sigma = sigma + 0.001 # increase sigma stepwise
}
Directed graph
Another perspective on the model’s properties is provided by its directed graph. A directed graph consists of a set of nodes that represent the variables of the model. Nodes are connected by directed edges. An edge directed from a node
## Create directed graph
# Construct auxiliary Jacobian matrix for 5 variables:
# endogenous: (1) Y, (2) K, (3) S, (4) I
# exogenous: (5) YE
#Y K S I YE
M_mat=matrix(c(0,0,1, 1, 1, # Y
0,0,0, 1, 0, # K
1,0,0, 0, 0, # S
1,1,0, 0, 1, # I
0,0,0, 0, 0),# YE
5, 5, byrow=TRUE)
# Create adjacency matrix from transpose of auxiliary Jacobian
A_mat=t(M_mat)
# Create directed graph from adjacency matrix
library(igraph)
dg=graph_from_adjacency_matrix(A_mat, mode="directed", weighted= NULL)
# Define node labels
V(dg)$name=c("Y", "K", "S", "I", expression(Y^E))
# Plot directed graph
plot(dg, main="Directed graph of Kaldor model", vertex.size=40, vertex.color="lightblue",
vertex.label.color="black", edge.arrow.size=0.3, edge.width=1.1, edge.size=1.2,
edge.arrow.width=1.2, edge.color="black", vertex.label.cex=1.2,
vertex.frame.color="NA", margin=-0.08)
##### Generate directed graph
# Import necessary libraries
import networkx as nx
# Construct auxiliary Jacobian matrix for 5 variables:
# Endogenous: (1) Y, (2) K, (3) S, (4) I
# Exogenous: (5) YE
= np.array([[0, 0, 1, 1, 1], # Y
M_mat 0, 0, 0, 1, 0], # K
[1, 0, 0, 0, 0], # S
[1, 1, 0, 0, 1], # I
[0, 0, 0, 0, 0]]) # YE
[
# Create adjacency matrix from transpose of auxiliary Jacobian
= M_mat.T
A_mat
# Create directed graph from adjacency matrix
= nx.DiGraph(A_mat)
G
# Define node labels
= {0: "Y", 1: "K", 2: "S", 3: "I", 4: r"$Y^E$"}
nodelabs
# Plot the graph using the specified approach
= nx.spring_layout(G, k=0.5)
pos =(8, 4))
plt.figure(figsize=200, node_color="lightblue",
nx.draw_networkx(G, pos, node_size="black", width=1.2, arrowsize=10,
edge_color='->', font_size=8, font_color="black",
arrowstyle=True, labels=nodelabs)
with_labels
"Directed Graph of Kaldor Model", fontsize=14)
plt.title( plt.show()
Figure 18.6 illustrates the endogenous cycle generated by the interaction between income
Analytical discussion
To analyse the dynamic properties of this discrete-time version of the Kaldor (1940) model, we first reduce it to a two-dimensional system in
Using the fact that
Next, we determine the steady states of the model by setting
From this, we can readily obtain:
Let
#### Plot equilibria
# Set parameter values
parm_1=1.2
parm_2=0.8
Y_E=10
# Create functions using Y as argument
f1a= function(Y){
parm_1*(Y - Y_E)
}
f1b= function(Y){
parm_2*(Y - Y_E)
}
f2= function(Y){
atan(Y - Y_E)
}
# Plot the functions
curve(f1a, from = 5, to = 15, col = 1, xlab="Y", ylab="" , main="",
lwd=1.5, n=10000, ylim=range(-2,2))
curve(f1b, from = 5, to = 15, col = 2, add=TRUE,
lwd=1.5,)
curve(f2, from = 5, to = 15, col = 3, add=TRUE,
lwd=1.5)
legend("bottomright", legend = c(expression(theta[1](Y-Y^E)),
expression(theta[2](Y-Y^E)), expression(arctan(Y-Y^E))),
col = 1:3, lwd = 2, bty = "n")
#### Plot equilibria
# Set parameter values
= 1.2
parm_1 = 0.8
parm_2 = 10
Y_E
# Define functions
def f1a(Y):
return parm_1 * (Y - Y_E)
def f1b(Y):
return parm_2 * (Y - Y_E)
def f2(Y):
return np.arctan(Y - Y_E)
# Create Y values for plotting
= np.linspace(5, 15, 1000)
Y_vals1 = np.linspace(5, 15, 1000)
Y_vals2
# Plot the functions
=(8, 6))
plt.figure(figsize='black', linewidth=1.5, label=r'$\theta_1(Y - Y^E)$')
plt.plot(Y_vals1, f1a(Y_vals1), color='red', linewidth=1.5, label=r'$\theta_2(Y - Y^E)$')
plt.plot(Y_vals1, f1b(Y_vals1), color='green', linewidth=1.5, label=r'$\arctan(Y - Y^E)$')
plt.plot(Y_vals2, f2(Y_vals2), color
# Customize the plot
"Y", fontsize=12)
plt.xlabel("", fontsize=12)
plt.ylabel(-2, 2)
plt.ylim(="lower right", frameon=False)
plt.legend(loc
# Show the plot
plt.show()
Let us focus on the
where
First, let us derive the condition under which the model produces cycles thanks to the eigenvalues being a pair of complex conjugates. The roots of the polynomial are:
so that the eigenvalues will be complex if
where
Let us focus on the last condition, which gives:
We now generate a plot that displays the cycle and the stability condition in the
#### Plot cycle and stability condition
# Set fixed parameter values
delta=0.2 # depreciation rate
gamma=0.6 # sensitivity of investment to deviations of actual from normal cap stock
# Create function for cycle condition
cyc= function(alpha, sigma){
(alpha*(1-sigma) + delta + gamma)^2 - 4*alpha*gamma
}
# Create function for stability condition
stab= function(alpha, sigma){
(1-delta -gamma)*(1-alpha*sigma) +alpha*(1-delta) - 1
}
# Create a grid of alpha and sigma values
alpha_vals = seq(0.5, 2, length.out = 100)
sigma_vals = seq(0, 1, length.out = 100)
grid=expand.grid(alpha = alpha_vals, sigma = sigma_vals)
# Evaluate the functions on the grid
cyc_vals = matrix(cyc(grid$alpha, grid$sigma), nrow = 100)
stab_vals = matrix(stab(grid$alpha, grid$sigma), nrow = 100)
# Plot the curves
contour(alpha_vals, sigma_vals, cyc_vals, levels = 0, col = 1, lwd = 2,
xlab = expression(alpha), ylab = expression(sigma), main = "")
contour(alpha_vals, sigma_vals, stab_vals, levels = 0, col = 2, lwd = 2, add = TRUE)
abline(h=1/(1+gamma/delta), col=3)
legend("topleft", legend = c("cycle condition",
"stability condition", "unique equilibrium condition"),
col = 1:3, lty = 1, cex=0.8, bty = "n")
### Plot cycle and stability condition
from matplotlib.lines import Line2D
# Set fixed parameter values
= 0.2 # Depreciation rate
delta = 0.6 # Sensitivity of investment to deviations from normal capital stock
gamma
# Define functions for cycle and stability conditions
def cyc(alpha, sigma):
return (alpha * (1 - sigma) + delta + gamma)**2 - 4 * alpha * gamma
def stab(alpha, sigma):
return (1 - delta - gamma) * (1 - alpha * sigma) + alpha * (1 - delta) - 1
# Create a grid of alpha and sigma values
= np.linspace(0.5, 2, 100)
alpha_vals = np.linspace(0, 1, 100)
sigma_vals = np.meshgrid(alpha_vals, sigma_vals)
alpha_grid, sigma_grid
# Evaluate the functions on the grid
= cyc(alpha_grid, sigma_grid)
cyc_vals = stab(alpha_grid, sigma_grid)
stab_vals
# Plot the curves
=(8, 6))
plt.figure(figsize
# Plot cycle condition contour
= plt.contour(alpha_vals, sigma_vals, cyc_vals, levels=[0], colors='black', linewidths=2)
cyc_contour
# Plot stability condition contour
= plt.contour(alpha_vals, sigma_vals, stab_vals, levels=[0], colors='red', linewidths=2)
stab_contour
# Plot unique equilibrium condition
= plt.axhline(y=1 / (1 + gamma / delta), color='green', linestyle='-', linewidth=2)
unique_eq_line
# Customize the plot
r'$\alpha$', fontsize=12)
plt.xlabel(r'$\sigma$', fontsize=12)
plt.ylabel(
# Create custom legend symbols
= [
legend_elements 0], [0], color='black', lw=2, label="Cycle condition"),
Line2D([0], [0], color='red', lw=2, label="Stability condition"),
Line2D([0], [0], color='green', lw=2, label="Unique equilibrium condition")
Line2D([
]
# Add legend with symbols
=legend_elements, loc="upper left", frameon=False, fontsize=9)
plt.legend(handles
False)
plt.grid( plt.show()
According to Figure 18.8, combinations of
These analytically derived conditions correspond to and illuminate further the numerical results in the bifurcation diagrams above. In Figure 18.4, it could be seen that for a fixed
Finally, we can also compute the eigenvalues and check the analytical stability and cycle conditions numerically.
### Stability analysis
# Set parameter values
alpha=1.2 # adjustment speed of output
delta=0.2 # depreciation rate
sigma=0.4 # propensity to save
gamma=0.6 # sensitivity of investment to deviations of actual from normal cap stock
# Construct Jacobian matrix evaluated at the Y*=Y_E steady state
J_base=matrix(c(1+alpha*(1-sigma), -alpha*gamma,
1, 1-delta-gamma),
2, 2, byrow=TRUE)
# Obtain eigenvalues
ev_base=eigen(J_base)
(evals_base = ev_base$values)
[1] 0.96+0.3773592i 0.96-0.3773592i
[1] 1.064
# Calculate modulus
(mod_base=Mod(evals_base[1]))
[1] 1.031504
#Check general stability conditions
print(1+tr+det>0)
[1] TRUE
print(1-tr+det>0)
[1] TRUE
print(1-det>0)
[1] FALSE
# Check analytical stability condition
((1-delta -gamma)*(1-alpha*sigma) +alpha*(1-delta)) < 1
[1] FALSE
These results confirm that the modulus of the complex eigenvalue is indeed larger than unity and the system thus unstable.
### Check cycle condition and compute cycle length
# Check analytical cycle condition
((alpha*(1-sigma) + delta + gamma)^2 - 4*alpha*gamma) < 0
[1] TRUE
# Save real and imaginary part of complex eigenvalue
re=Re(evals_base[1])
im=Im(evals_base[1])
# Calculate cycle length
L=(2*pi)/(acos(re/mod_base))
L
[1] 16.77624
###### Stability analysis
# Set parameter values
= 1.2 # Adjustment speed of output
alpha = 0.2 # Depreciation rate
delta = 0.4 # Propensity to save
sigma = 0.6 # Sensitivity of investment to deviations from normal capital stock
gamma
# Construct Jacobian matrix evaluated at the Y* = Y_E steady state
= np.array([
J_base 1 + alpha * (1 - sigma), -alpha * gamma],
[1, 1 - delta - gamma]
[
])
# Obtain eigenvalues
= np.linalg.eig(J_base)
evals_base, _
# Print eigenvalues
print("Eigenvalues:", evals_base)
# Obtain determinant and trace
= np.trace(J_base) # Trace
tr = np.linalg.det(J_base) # Determinant
det
# Print determinant
print(f"Determinant: {det}")
# Calculate and print modulus of the first eigenvalue
= abs(evals_base[0])
mod_base print(f"Modulus of first eigenvalue: {mod_base}")
# Check general stability conditions
print(f"1 + tr + det > 0: {1 + tr + det > 0}")
print(f"1 - tr + det > 0: {1 - tr + det > 0}")
print(f"1 - det > 0: {1 - det > 0}")
# Check analytical stability condition
print(((1 - delta - gamma) * (1 - alpha * sigma) + alpha * (1 - delta)) < 1)
### Check cycle condition and compute cycle length
# Check analytical cycle condition
print(((alpha * (1 - sigma) + delta + gamma)**2 - 4 * alpha * gamma) < 0)
# Save real and imaginary parts of complex eigenvalue
= np.real(evals_base[0])
re = np.imag(evals_base[0])
im
# Calculate cycle length
= (2 * np.pi) / np.arccos(re / mod_base)
L print(f"Cycle length: {L}")
The analytical cycle condition confirms that the eigenvalues of the system will be complex and thus generate cycles. The implied cycle length is around 17 periods.
References
More precisely, Samuelson (1939)’s model generates shock-independent cycles only for a very specific parameter combination, whereas Kaldor (1940)’s model generates endogenous cycles for a much broader set of parameters.↩︎
The first 10 periods were excluded from the computation as these are driven by the adjustment from the arbitrary initialisation.↩︎
In discrete-time dynamic systems, the Hopf bifurcation is also called Neimark-Sacker bifurcation.↩︎
To increase computational efficiency, we simulate a reduced-form version of the model in
and only that is derived in the analytical discussion below.↩︎See Bischi et al. (2001) for a comprehensive mathematical analysis of all possible equilibria of the model.↩︎
See also Stockhammer et al. (2019) on this condition for cycles.↩︎
To plot the conditions, we replace the inequalities by equality signs and solve for 0.↩︎