A Simple Ramsey Model
Introduction
A known issue with the Solow model is that the savings rate is exogenous. This means the ratio of consumption-to-income
This note presents a simple Ramsey model (it offers a simplified discussion, does not discuss shocks, and does not add other features). For a more complete and detailed discussion see an advanced macroeconomics textbook.
The model is populated by a representative household that maximizes its utility and by a representative firm that maximizes its profits. The model assumes a competitive market (no monopoly power, agents are price takers, etc.) The household provides labor services to the firm in exchange of a wage. The wage can be used to consume goods or become savings (accumulate assets). The model assumes a closed economy with no government. Finally, the model is presented in continuous (rather than discrete) time. A dot on top of a variable denotes its instantaneous time-change
The representative household
Assume a large number of identical infinitely-lived households. Because these households are identical, we can use a representative household to populate the model. The assumptions that this households are infinitely lived captures a continuum of generations. The representative household has
The household utility
The household utility
where
The parameter
The household budget constraint
The household budget constrained is composed of assets
Dividing by
To avoid the situation where the household borrows money indefinitely, the model also imposes a “no Ponzi game condition”. Intuitively, at the end of the household life
where
The household optimization problem
The household is facing a dynamic optimization problem. It needs to set the path of consumption that maximizes the present value of its life utility. The optimization problem can be described with the following equations (where
The optimization problem is given an initial asset value of
where
The first-order-conditions (FOC) of the hamiltonian are:
Now we proceed to use the first equation to get
Next make now two substitutions: (1) The first FOC in the third FOC, and (2) this last derivative in the third FOC as well.
The last expression is the Euler equation, which defines the path for
The Euler equation also includes the elasticity of marginal utility:
where
Then, the Euler equation from the household maximization problem becomes
Conveniently, we can rewrite the Euler equation depict the consumption path that maximizes the household utility.
This is one of the two equations that describes Ramsey model. The second equation is derived from the firms profit maximization problem (next section).
The following code plots the utility function for different levels of
#%% | *** CELL 1 ***
"============================================================================"
"1|IMPORT PACKAGES"
import numpy as np # Package for scientific computing with Python
import matplotlib.pyplot as plt # Matplotlib is a plotting library
"============================================================================"
"2|DEFINE PARAMETERS AND ARRAYS"
# PARAMETERS
# Code parameters
size = 5 # Model domain
steps = size*100 # Number of dots in the domain
dpi = 300 # Figure quality
# Array
c = np.linspace(0, size, steps) # Create array of consumption
"============================================================================"
"3|DEFINE UTILITY FUNCTION"
# Production function
def u(x, CRRA):
if CRRA == 1:
u = np.log(x)
else:
u = ((x)**(1-CRRA)-1)/(1-CRRA)
return u
"============================================================================"
"4|CALCULATE UTILITY FUNCTIONS FOR DIFFERENT VALUES OF THETA"
theta = [0.25, 0.75, 1, 1.5, 2]
# Ignore c[0] = 0 to avoid dividing by zero
u1 = u(c[1:], theta[0])
u2 = u(c[1:], theta[1])
u3 = u(c[1:], theta[2])
u4 = u(c[1:], theta[3])
u5 = u(c[1:], theta[4])
"============================================================================"
"5|PLOT UTILITY FUNCTIONS"
### AXIS RANGE
axis_range = [0, size, -10, 5]
### BUILD PLOT AND POPULIATE WITH LOCI LINES
fig, ax = plt.subplots(figsize=(10, 7), dpi=dpi)
ax.plot(c[1:], u1, alpha = 0.75, label = r'$\theta=%.2f$' %theta[0])
ax.plot(c[1:], u2, alpha = 0.75, label = r'$\theta=%.2f$' %theta[1])
ax.plot(c[1:], u3, alpha = 0.75, label = r'$\theta=%.2f$' %theta[2])
ax.plot(c[1:], u4, alpha = 0.75, label = r'$\theta=%.2f$' %theta[3])
ax.plot(c[1:], u5, alpha = 0.75, label = r'$\theta=%.2f$' %theta[4])
### AXIS
ax.axhline(0, color = 'k') # Add horizontal axis
ax.axvline(0, color = 'k') # Add vertical axis
ax.yaxis.set_major_locator(plt.NullLocator()) # Hide ticks
ax.xaxis.set_major_locator(plt.NullLocator()) # Hide ticks
### TEXT
ax.text(size-0.10, -0.5, r'$c$', color = 'k') # Manually add x-axis label
ax.text( -0.25, 4.5, r'$u(c)$') # Manually add y-axis label
# SETTINGS
plt.box(False) # Hide axis
plt.legend(loc=0, frameon=False)
plt.axis(axis_range)
plt.show()
The representative firm
The representative firm uses a labor-augmenting technology production function
In terms of effective labor
The rate of return on capital equals the gross rate of return
The FOC for the firm with respect to capital per effective unit of labor is:
In equilibrium the firm’s profit is zero, therefore:
Equilibrium
In equilibrium, the assets owned by the household coincide with the capital per unit of labor. Recall that in this model households own the capital that is rented to the firms. Also, note that assets
Knowing that
In equilibrium,
From the second equation we can also derive the value of
The model has one stable saddle path that converges to equilibrium. This saddle-path is unstable, if the model is outside this path by the smallest distance, then the model diverges away from equilibrium.
The code
The Python
code starts by calculating and plotting the the equilibrium, gold values, and loci lines. The code plots this in two graphs. The first one shows the loci lines with five sample paths (in red). Each one of these sample paths depicts the unstable dynamics of the model. The second graph plots the stable-path (in blue).
Estimating the stable-path
A common way to estimate the stable-path is with a “shooting” algorithm. This approach shoots a number of paths until it finds one that is close enough the actual stable path. An arbitrarily chosen tolerance level defines what close enough means for the algorithm.
This note uses a simple code and methodology to illustrate this type of numerical method. It is not intended to be the more efficient one. The stable path is estimated in two halves. One estimation for the stable path section to the left of the equilibrium point, and another estimation for the stable path to the right of the equilibrium point. These two estimations are coded as two different functions: (1) backward shoot and (2) forward shoot respectively. Alternatively, one simple “shoot” function could include both initial locations. Let
These type of iterations can be time consuming depending on how many shoots the code is going to perform (which depends on how big a correction is when a shoot misses the steady-state, how small the tolerance error is, how far away from equilibrium the first shot is, and so on). A more sophisticated code could reduce the number of attempts and save computational time through different numerical methods and techniques (take an advanced math course).
Since we know that is very unlikely that our “shoots” will exactly hit the steady state $(k^, c(k^)$, we need to define a success criteria. The success criteria is when the shoots hits close enough to the target. We need to define a circle around the equilibrium that plays the role of being the tolerance level. Once we find a shoot that hits inside this circle, the shoots is considered a success and an approximation good enough for the estimation purposes.
The distance of a point to the steady-state is:
The shooting functions have two inputs. The initial level of
The forward shot
Let’s start with the forward shoot function. The initial point is one step below the loci function. We know from the first plot blow that is very likely that this initial shoot is too high, and therefore the path will go above
There is a third break, which is a cap on the number of iterations the loop can perform. When building a code like this, this type of break can help avoid a mistake that sends the code into an infinite loop.
If the path produces a value of
The backward shot
The backward shoot function follow a similar structure than the forward shoot. Yet, because the starting point is different, the break points have to be modified. However, there is a difference in the starting point. Rather than starting at the loci level given the initial on step above
The forward shoot loop has two break points. The first one is if the shoot produces a point inside the tolerance circle. The second one is if the path produces a value of
If the shoot produces a value of
#%% *** CELL 2 ***
"============================================================================"
"6|DEFINE PARAMETERS AND ARRAYS"
# PARAMETERS
# Code parameters
k_size = 150 # Model domain
steps = k_size*100 # Number of "dots" in the domain
# Ramsey model aprameters
alpha = 0.30 # Output elasticity of capital
delta = 0.35 # Depreciation rate
rho = 0.35 # Time preference
n = 0.05 # Population growth rate
g = 0.05 # TFP growth rate
theta = 0.8 # Coefficient or Relative Risk Aversion
# ARRAY
k_array = np.linspace(0, k_size, steps) # Create array of k
"============================================================================"
"7|DEFINE FUNCTIONS"
# PRODUCTION FUNCTION
def y(k):
y = k**alpha
return y
# MARGINAL PRODUCT OF CAPITAL
def y_prime(k):
y_prime = alpha*k**(alpha-1)
return y_prime
# CONSUMPTION
def consumption(k):
c = y(k) - (n + g + delta)*k
return c
# MOTION FUNCTION OF CONSUMPTION
def cdot(k, c):
cdot = 1/theta * (y_prime(k) - delta - rho) * c
return cdot
# MOTION FUNCTION OF CAPITAL
def kdot(k, c):
kdot = y(k) - c - (n + g + delta)*k
return kdot
"============================================================================"
"8|CALCULATE STEADY-STATE AND GOLD VALUES"
# STEADY STATE VALUES
k_star = (delta + rho)**(1/alpha)
c_star = consumption(k_star)
y_star = y(k_star)
s_star = (y_star - c_star)/y_star
# GOLD VALUES
k_gold = (alpha/(n+g+delta))**(1/(1-alpha))
c_gold = consumption(k_gold)
y_gold = y(k_gold)
s_gold = (y_gold - c_gold)/y_gold
"============================================================================"
"9|CALCULATE LOCI FUNCTIONS"
k_loci = k_star
c_loci = consumption(k_array)
"============================================================================"
"10|CALCULATE SAMPLE PATHS"
k00, k01, k02 = k_star*0.25, k_star*0.50, k_star*2.2
c0 = [consumption(k00)*0.25, consumption(k00), c_star*1.25,
consumption(k01)*0.40, consumption(k01)*1.50]
def sample_path(k0, c0, n):
path = np.zeros(shape=(n, 2))
path[0, 0] = k0
path[0, 1] = c0
for j in range(n-1):
if path[j,0] < 0: # Stop if motion goes off-chart
break
else:
path[j+1, 0] = path[j, 0] + kdot(path[j, 0], path[j, 1])*0.25
path[j+1, 1] = path[j, 1] + cdot(path[j, 0], path[j, 1])*0.25
return path
path1 = sample_path(k00, c0[0], 30)
path2 = sample_path(k00, c0[1], 30)
path3 = sample_path(k01, c0[2], 30)
path4 = sample_path(k02, c0[3], 30)
path5 = sample_path(k02, c0[4], 30)
"============================================================================"
"11|PLOT RAMSEY MODEL WITH SAMPLE PATHS"
# VALUE OF k SUCH THAT c = 0
k_zero = (1/(n + g + delta))**(1/(1-alpha))
# AXIS RANGE
y_max = np.max(c_loci)*1.7
x_max = k_zero*1.2
axis_range = [0, x_max, -0.1, y_max]
### BUILD PLOT AND POPULIATE WITH LOCI LINES
fig, ax = plt.subplots(figsize=(10, 7), dpi=dpi)
ax.plot(k_array, c_loci, color="k", alpha = 0.8)
ax.axvline(k_loci, color="k", alpha = 0.8)
### SAMPLE PATHS
ax.plot(path1[:,0], path1[:,1], "r--", alpha = 0.7)
ax.plot(path2[:,0], path2[:,1], "r--", alpha = 0.7)
ax.plot(path3[:,0], path3[:,1], "r--", alpha = 0.7)
ax.plot(path4[:,0], path4[:,1], "r--", alpha = 0.7)
ax.plot(path5[:,0], path5[:,1], "r--", alpha = 0.7)
### ADD DOTS AT BEGINNING OF SAMPLE PATHS
plt.plot(k00, c0[0], 'ro', alpha = 0.7)
plt.plot(k00, c0[1], 'ro', alpha = 0.7)
plt.plot(k01, c0[2], 'ro', alpha = 0.7)
plt.plot(k02, c0[3], 'ro', alpha = 0.7)
plt.plot(k02, c0[4], 'ro', alpha = 0.7)
### ADD MOTION ARROWS
plt.arrow(k00, c0[0], 0, 0.10, head_width=0.03,head_length=0.02,fc='r',ec='r')
plt.arrow(k00, c0[0], 0.18, 0, head_width=0.02,head_length=0.03,fc='r',ec='r')
plt.arrow(k01, c0[2], 0, 0.10, head_width=0.03,head_length=0.02,fc='r',ec='r')
plt.arrow(k01, c0[2],-0.08, 0, head_width=0.02,head_length=0.03,fc='r',ec='r')
plt.arrow(k02, c0[3], 0,-0.08, head_width=0.03,head_length=0.02,fc='r',ec='r')
plt.arrow(k02, c0[3], 0.22, 0, head_width=0.02,head_length=0.03,fc='r',ec='r')
plt.arrow(k02, c0[4], 0,-0.12, head_width=0.03,head_length=0.02,fc='r',ec='r')
plt.arrow(k02, c0[4],-0.12, 0, head_width=0.02,head_length=0.03,fc='r',ec='r')
### AXIS
ax.axvline(0, color="k") # y-axis
ax.axhline(0, color="k") # x-axis
ax.yaxis.set_major_locator(plt.NullLocator()) # Hide ticks
ax.xaxis.set_major_locator(plt.NullLocator()) # Hide ticks
### TEXT
plt.text(x_max*0.98, -0.05 , r'$\hat{k}$' , color = 'k') # x-axis label
plt.text(-0.15 , y_max*0.95 , r'$\hat{c}$' , color = 'k') # y-axis label
plt.text(k_star*1.05, y_max*0.95, r'$\hat{c}=0$', color = "k") # c loci label
plt.text(k_zero*0.95, 0.07 , r'$\hat{k}=0$', color = "k") # k loci label
# SETTINGS
plt.box(False) # Hide axis
plt.axis(axis_range)
plt.show()
"============================================================================"
"12|STABLE-PATH: FORWARD AND BACKWARD SHOOTING"
def forward_shoot(k0, step):
# Set conditions for initial shoot
tol = 1.0e-10 # Tolerance level
c0 = consumption(k0) - step # Initial consumption shoot value
error = np.abs(((k0 - k_star)**2 + (c0 - c_star)**2)**0.5)
path = np.zeros(shape=(1, 2))
path[0,0] = k0
path[0,1] = c0
# Start loop (forward shoot)
count = 0
while 1:
k1 = path[count, 0]
c1 = path[count, 1]
k2 = k1 + kdot(k1, c1)*0.1
c2 = c1 + cdot(k1, c1)*0.1
error = np.abs(((k2 - k_star)**2 + (c2 - c_star)**2)**0.5)
path = np.append(path, [[k2, c2]], axis = 0)
count = count + 1
# Check if this is the stable-path
if error < tol:
break
# Set up code break and reset the shoot
if c2 > c_star:
path = np.zeros(shape=(1,2)) # Empty collected path data
c0 = c0 - step # Move the initial shot by one step
path[0,0] = k0
path[0,1] = c0
count = 0
# Break code when k < k*
if k2 > k_star:
break
# Put a limit of iterations to the code
if count > 100:
break
return path
def backward_shoot(k0, step):
# Set conditions for initial shoot
tol = 1.0e-10 # Tolerance level
c0 = consumption(k_gold) * 1.5 # Initial consumptoin shoot value
error = np.abs(((k0 - k_star)**2 + (c0 - c_star)**2)**0.5)
path = np.zeros(shape=(1, 2))
path[0,0] = k0
path[0,1] = c0
# Start loop (forward shoot)
count = 0
while 1: # Infinite loop with break conditions inside
k1 = path[count, 0]
c1 = path[count, 1]
k2 = k1 + kdot(k1, c1)*0.01
c2 = c1 + cdot(k1, c1)*0.01
error = np.abs(((k2 - k_star)**2 + (c2 - c_star)**2)**0.5)
path = np.append(path, [[k2, c2]], axis = 0)
count = count + 1
# Check if this is the stable-path
if error < tol:
break
# Set up code break and reset the shoot
if c2 < c_star:
path = np.zeros(shape=(1, 2))
c0 = c0 + step
path[0,0] = k0
path[0,1] = c0
count = 0
# Break code when k < k*
if k2 < k_star:
break
# Put a limit of iterations to the code
if count > 400:
break
return path
stable_L = forward_shoot(k00, 0.000001) # Execute shooting algorithm
stable_R = backward_shoot(k_star*2.00, 0.000001) # Execute shooting algorithm
# Be patient ...
"============================================================================"
"13|PLOT RAMSEY MODEL WITH STABLE PATH"
# VALUE OF k SUCH THAT c = 0
k_zero = (1/(n + g + delta))**(1/(1-alpha))
# AXIS RANGE
y_max = np.max(c_loci)*1.7
x_max = k_zero*1.2
axis_range = [0, x_max, -0.1, y_max]
### BUILD PLOT AND POPULIATE WITH LOCI LINES
fig, ax = plt.subplots(figsize=(10, 7), dpi=dpi)
ax.plot(k_array, c_loci, color="k", alpha = 0.8)
ax.axvline(k_loci, color="k", alpha = 0.8)
### STABLE PATH
ax.plot(stable_L[:,0], stable_L[:,1], "b--", alpha = 0.7)
ax.plot(stable_R[:,0], stable_R[:,1], "b--", alpha = 0.7)
### AXIS
ax.axvline(0, color="k") # y-axis
ax.axhline(0, color="k") # x-axis
ax.yaxis.set_major_locator(plt.NullLocator()) # Hide ticks
ax.xaxis.set_major_locator(plt.NullLocator()) # Hide ticks
### TEXT
plt.text(x_max*0.98, -0.05 , r'$\hat{k}$' , color = 'k') # x-axis label
plt.text(-0.15 , y_max*0.95 , r'$\hat{c}$' , color = 'k') # y-axis label
plt.text(k_star*1.05, y_max*0.95, r'$\hat{c}=0$', color = "k")
plt.text(k_zero*0.95, 0.07 , r'$\hat{k}=0$', color = "k")
plt.text(k_star*1.05, -0.05 , r'$k^*$' , color = 'k')
plt.text(-0.15, c_star , r'$c^*$' , color = 'k')
ax.axhline(c_star, 0, k_star/axis_range[1], linestyle=":", color = "grey")
# SETTINGS
plt.box(False) # Hide axis
plt.axis(axis_range)
plt.show()