#!/usr/bin/env python # coding: utf-8 # # Foundations of Computational Economics #29 # # by Fedor Iskhakov, ANU # # # ## Coding up the Rust model of bus engine replacement # # # # # [https://youtu.be/QzOVgKIUBqg](https://youtu.be/QzOVgKIUBqg) # # Description: Implementation of Rust model in infinite horizon with value function iterations solver # ### Bellman equation in expected value function space # # $ EV(x,d) $ = the expected value function # # $$ # \begin{eqnarray} # EV(x,d) &=& \sum_{x' \in X} \log \big( \exp[v(x',0)] + \exp[v(x',1)] \big) \pi(x'|x,d) \\ # v(x,d) &=& u(x,d) + \beta EV(x,d) # \end{eqnarray} # $$ # # - $ x \in \{x_1, \dots x_n \} $, the state space is given by $ n $ grid points of mileage # - $ d \in \{0,1\} $, the choice space is given by two points # - $ EV(x,d) $ is $ n $ by $ 2 $ matrix # #### Instantaneous payoffs # # Given by the cost function that depends on the choice # # $$ # u(x,d,\theta_1)=\left \{ # \begin{array}{ll} # -RC-c(0,\theta_1) & \text{if }d_{t}=\text{replace}=1 \\ # -c(x,\theta_1) & \text{if }d_{t}=\text{keep}=0 # \end{array} \right. # $$ # # - $ RC $ = replacement cost # - $ c(x,\theta_1) = 0.001 \cdot \theta_1 x $ = cost of maintenance # - $ \theta_1 $ = preference parameters # #### Transition matrix for mileage # # If not replacing ($ d=0) $ # # $$ # \Pi(d=0)_{n x n} = # \begin{pmatrix} # \theta_{20} & \theta_{21} & \theta_{22} & 0 & \cdot & \cdot & \cdot & 0 \\ # 0 & \theta_{20} & \theta_{21} & \theta_{22} & 0 & \cdot & \cdot & 0 \\ # 0 & 0 &\theta_{20} & \theta_{21} & \theta_{22} & 0 & \cdot & 0 \\ # \cdot & \cdot & \cdot & \cdot & \cdot & \cdot & \cdot & \cdot \\ # 0 & \cdot & \cdot & 0 & \theta_{20} & \theta_{21} & \theta_{22} & 0 \\ # 0 & \cdot & \cdot & \cdot & 0 & \theta_{20} & \theta_{21} & \theta_{22} \\ # 0 & \cdot & \cdot & \cdot & \cdot & 0 & \theta_{20} & 1-\theta_{20} \\ # 0 & \cdot & \cdot & \cdot & \cdot & \cdot & 0 & 1 # \end{pmatrix} # $$ # #### Transition matrix for mileage # # If replacing ($ d=1) $ # # $$ # \Pi(d=1)_{n x n} = # \begin{pmatrix} # \theta_{20} & \theta_{21} & \theta_{22} & 0 & \cdot & \cdot & \cdot & 0 \\ # \theta_{20} & \theta_{21} & \theta_{22} & 0 & \cdot & \cdot & \cdot & 0 \\ # \theta_{20} & \theta_{21} & \theta_{22} & 0 & \cdot & \cdot & \cdot & 0 \\ # \cdot & \cdot & \cdot & \cdot & \cdot & \cdot & \cdot & \cdot \\ # \theta_{20} & \theta_{21} & \theta_{22} & 0 & \cdot & \cdot & \cdot & 0 \\ # \theta_{20} & \theta_{21} & \theta_{22} & 0 & \cdot & \cdot & \cdot & 0 \\ # \theta_{20} & \theta_{21} & \theta_{22} & 0 & \cdot & \cdot & \cdot & 0 \\ # \end{pmatrix} # $$ # #### Bellman operator and VFI # # $$ # \begin{eqnarray} # T^*(EV)(x,d) &\equiv& \sum_{x' \in X} \log \big( \exp[v(x',0)] + \exp[v(x',1)] \big) \pi(x'|x,d) \\ # v(x,d) &=& u(x,d) + \beta EV(x,d) # \end{eqnarray} # $$ # # 1. Start with arbitrary guess for \$EV(x,d)\$ # 1. Apply \$T^*\$ operator # 1. Check for (uniform) convergence # 1. If not converged to a given level of tolerance, return to step 2, otherwise finish. # #### One more thing # # *It is always useful to examine the structure of the problem for insights into better code structure!* # # - Bus with just replaced engine is identical to the new bus from the model’s point of view # $ \Rightarrow EV(x,d = \text{replace}) = EV(0,d = \text{keep}) $ for all $ x $ # - and instead of $ EV(x,d) $ to be $ n $ by $ 2 $ matrix, it is sufficient to work with a vector # $ EV(x) $ with $ n $ elements # - we also only need one transition probability matrix then # - When computing **logsum** functions # # # $$ # \log \big( \exp[v(x',0)] + \exp[v(x',1)] \big) # $$ # # - we can avoid computing exponential of large numbers by using the equivalent # # # $$ # M + \log \big( \exp[v(x',0)-M] + \exp[v(x',1)-M] \big) # $$ # In[1]: # THIS IS DEVELOPPED IN THE VIDEO import numpy as np import matplotlib.pyplot as plt get_ipython().run_line_magic('matplotlib', 'inline') class zurcher(): '''Harold Zurcher bus engine replacement model class, VFI version''' def __init__(self, n = 175, # number of state points RC = 11.7257, # replacement cost c = 2.45569, # parameter of maintance cost (theta_1) p = [0.0937,0.4475,0.4459,0.0127], # probabilities of transitions (theta_2) beta = 0.9999): # discount factor '''Init for the Zurcher model object''' assert sum(p)<=1.0, 'Transition probability parameters must sum up to <1' self.RC, self.c, self.p, self.beta, self.n= RC, c, p, beta, n @property def n(self): '''Attrinute getter for n''' return self.__n @n.setter def n(self, value): '''Attribute n setter''' self.__n = value self.grid = np.arange(self.__n) self.trpr = self.__transition_probs() def __repr__(self): '''String representation of the Zurcher model''' return 'Rust model at id={}'.format(id(self)) def __transition_probs(self): '''Computing the transision probability matrix''' trpr = np.zeros((self.__n,self.__n)) # init probs = self.p + [1-sum(self.p)] # ensure sum up to 1 for i,p in enumerate(probs): trpr += np.diag([p]*(self.__n-i),k=i) trpr[:,-1] = 1.-np.sum(trpr[:,:-1],axis=1) return trpr def bellman(self,ev0): '''Bellman operator for the model''' x = self.grid # points in the next period state mcost = -0.001*x*self.c vx0 = mcost + self.beta * ev0 # vector of v(x,0) vx1 = -self.RC + mcost[0] + self.beta * ev0[0] # vector of v(x,1) M = np.maximum(vx0,vx1) logsum = M + np.log(np.exp(vx0-M) + np.exp(vx1-M)) ev1 = self.trpr @ logsum[:,np.newaxis] ev1 = ev1.ravel() # recompute v(x,d) for current period choice probabilities vx0 = mcost + self.beta * ev1 # vector of v(x,0) vx1 = -self.RC + mcost[0] + self.beta * ev1[0] # vector of v(x,1) pr1 = 1 / (np.exp(vx0-vx1)+1) # choice prob to repace return ev1, pr1 def solve_vfi(self,tol=1e-6,maxiter=100,callback=None): '''Solves the Rust model using value function iterations ''' ev0 = np.zeros(self.n) # initial point for VFI for i in range(maxiter): # main loop ev1, pr1 = self.bellman(ev0) # update approximation err = np.amax(np.abs(ev0-ev1)) if callback != None: callback(iter=i,err=err,ev1=ev1,ev0=ev0,pr1=pr1,model=self) if err0) Frechet derivative of Bellman operator (if output>1) ''' # EV0 is current approximation of EV on the fixed grid # For d=0 it holds values for all mileages # For d=1 (replacement) we use the first value EV0[0] # So, instead of EV(x,d) for d=0,1, we can use only one vector! assert np.all(ev0.shape==(self.n,1)),'Expecting EV as column vector' x = self.grid.reshape((self.n,1)) # states (in the next period), column vector c = 0.001*self.c*x # maintenance cost in all states v0 = -c + self.beta*ev0 # value of not replacing v1 = -c[0] -self.RC + self.beta*ev0[0] # value of replacing # recenter the values for numerical stability of logsum !!!!!!!!!!!!!!!! maxv = np.maximum(v0,v1) logsum = maxv + np.log(np.exp(v0-maxv) + np.exp(v1-maxv)) ev1 = self.P1 @ logsum # matrix multiplication, result as column vector if output == 0: return ev1 # keep (no replacement) choice probability pk = 1/(1+np.exp(v1-v0)) if output == 1: return ev1,pk def solve_vfi(self, maxiter=1000, tol=1e-6, callback=None): '''Solves the model using successive approximations (VFI)''' ev0 = np.full((self.n,1),0) # initial guess of EV for iter in range(maxiter): ev1,pk = self.bellman(ev0,output=1) stp = np.max(abs(ev1-ev0)) if callback: if iter==0: stp0=1.0 callback(iter,self,ev1,pk,stp,stp/stp0) # callback for making plots if stp < tol: break ev0=ev1 stp0=stp else: # when i went up to maxiter print('No convergence: maximum number of iterations achieved!') return ev1,pk def solve_show(self,solver='vfi',maxiter=1000,tol=1e-6,**kvargs): '''Illustrate solution''' fig1, (ax1,ax2) = plt.subplots(1,2,figsize=(14,8)) ax1.grid(b=True, which='both', color='0.65', linestyle='-') ax2.grid(b=True, which='both', color='0.65', linestyle='-') ax1.set_xlabel('Mileage grid') ax2.set_xlabel('Mileage grid') ax1.set_title('Value function') ax2.set_title('Probability of keeping the engine') def callback(iter,mod,ev,pk,stp,dstp): if iter==0: print('%4s %16s %16s'%('iter','err','err(i)/err(i-1)')) print('-'*40) print('%4d %16.12f %16.12f'%(iter,stp,dstp)) ax1.plot(mod.grid,ev,color='k',alpha=0.25) ax2.plot(mod.grid,pk,color='k',alpha=0.25) if solver=='vfi': ev,pk = self.solve_vfi(maxiter=maxiter,tol=tol,callback=callback,**kvargs) elif solver=='nk': ev,pk = self.solve_nk(maxiter=maxiter,tol=tol,callback=callback,**kvargs) elif solver=='poly': ev,pk = self.solve_poly(maxiter=maxiter,tol=tol,callback=callback,**kvargs) else: print('Unknown solver') return None # add solutions ax1.plot(self.grid,ev,color='r',linewidth=2.5) ax2.plot(self.grid,pk,color='r',linewidth=2.5) plt.show() return ev,pk # In[3]: # investigate how parts of the code work: model = zurcher(RC=.5,n=12,p=[0.65,0.2,0.1]) # model instance print('Model grid:\n',model.grid) print(model) # string representation print('Transition probabilities conditional on not replacing:\n',model.P1) print('Transition probabilities conditional on replacing:\n',model.P2) ev,pk = model.bellman(np.full((model.n,1),0),output=1) print('Bellman one run:\n',ev) print('Probability of keeping:\n',pk) # In[4]: # solve Harold Zurcher model for different parameters m = zurcher(beta=0.9,RC=5.0) ev,pk = m.solve_show(maxiter=500,tol=1e-4) # ### Further learning resources # # - Matlab implementation of full solver and Rust estimator (NFXP) [https://github.com/dseconf/DSE2019/tree/master/02_DDC_SchjerningIskhakov](https://github.com/dseconf/DSE2019/tree/master/02_DDC_SchjerningIskhakov)