├── environment.yml ├── README.md ├── heavy_tails.ipynb ├── troubleshooting.ipynb ├── cross_product_trick.ipynb ├── intro.ipynb ├── status.ipynb ├── schelling.ipynb ├── egm_policy_iter.ipynb ├── rand_resp.ipynb ├── os_egm.ipynb ├── cake_eating_egm.ipynb ├── inventory_dynamics.ipynb ├── cake_eating_egm_jax.ipynb ├── os_egm_jax.ipynb └── mccall_correlated.ipynb /environment.yml: -------------------------------------------------------------------------------- 1 | name: lecture-python 2 | channels: 3 | - default 4 | dependencies: 5 | - python=3.8 6 | - anaconda 7 | 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lecture-python.notebooks 2 | 3 | [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/QuantEcon/lecture-python.notebooks/master) 4 | 5 | Notebooks for https://python.quantecon.org 6 | 7 | **Note:** This README should be edited [here](https://github.com/quantecon/lecture-python.myst/_notebook_repo) 8 | -------------------------------------------------------------------------------- /heavy_tails.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "3b1e3870", 6 | "metadata": {}, 7 | "source": [ 8 | "# Heavy-Tailed Distributions" 9 | ] 10 | }, 11 | { 12 | "cell_type": "markdown", 13 | "id": "c7252f5d", 14 | "metadata": {}, 15 | "source": [ 16 | "# New website\n", 17 | "\n", 18 | "We have moved this lecture to a new lecture series.\n", 19 | "\n", 20 | "See [An Undergraduate Lecture Series for the Foundations of Computational Economics](https://intro.quantecon.org)" 21 | ] 22 | } 23 | ], 24 | "metadata": { 25 | "date": 1710733971.7124138, 26 | "filename": "heavy_tails.md", 27 | "kernelspec": { 28 | "display_name": "Python", 29 | "language": "python3", 30 | "name": "python3" 31 | }, 32 | "title": "Heavy-Tailed Distributions" 33 | }, 34 | "nbformat": 4, 35 | "nbformat_minor": 5 36 | } -------------------------------------------------------------------------------- /troubleshooting.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "304b7787", 6 | "metadata": {}, 7 | "source": [ 8 | "\n", 9 | "\n", 10 | "
\n", 11 | " \n", 12 | " \"QuantEcon\"\n", 13 | " \n", 14 | "
" 15 | ] 16 | }, 17 | { 18 | "cell_type": "markdown", 19 | "id": "a813234b", 20 | "metadata": {}, 21 | "source": [ 22 | "# Troubleshooting" 23 | ] 24 | }, 25 | { 26 | "cell_type": "markdown", 27 | "id": "18baae65", 28 | "metadata": {}, 29 | "source": [ 30 | "## Contents\n", 31 | "\n", 32 | "- [Troubleshooting](#Troubleshooting) \n", 33 | " - [Fixing Your Local Environment](#Fixing-Your-Local-Environment) \n", 34 | " - [Reporting an Issue](#Reporting-an-Issue) " 35 | ] 36 | }, 37 | { 38 | "cell_type": "markdown", 39 | "id": "78617204", 40 | "metadata": {}, 41 | "source": [ 42 | "This page is for readers experiencing errors when running the code from the lectures." 43 | ] 44 | }, 45 | { 46 | "cell_type": "markdown", 47 | "id": "a1af96ae", 48 | "metadata": {}, 49 | "source": [ 50 | "## Fixing Your Local Environment\n", 51 | "\n", 52 | "The basic assumption of the lectures is that code in a lecture should execute whenever\n", 53 | "\n", 54 | "1. it is executed in a Jupyter notebook and \n", 55 | "1. the notebook is running on a machine with the latest version of Anaconda Python. \n", 56 | "\n", 57 | "\n", 58 | "You have installed Anaconda, haven’t you, following the instructions in [this lecture](https://python-programming.quantecon.org/getting_started.html)?\n", 59 | "\n", 60 | "Assuming that you have, the most common source of problems for our readers is that their Anaconda distribution is not up to date.\n", 61 | "\n", 62 | "[Here’s a useful article](https://www.anaconda.com/blog/keeping-anaconda-date)\n", 63 | "on how to update Anaconda.\n", 64 | "\n", 65 | "Another option is to simply remove Anaconda and reinstall.\n", 66 | "\n", 67 | "You also need to keep the external code libraries, such as [QuantEcon.py](https://quantecon.org/quantecon-py/) up to date.\n", 68 | "\n", 69 | "For this task you can either\n", 70 | "\n", 71 | "- use `pip install --upgrade quantecon` on the command line, or \n", 72 | "- execute `!pip install --upgrade quantecon` within a Jupyter notebook. \n", 73 | "\n", 74 | "\n", 75 | "If your local environment is still not working you can do two things.\n", 76 | "\n", 77 | "First, you can use a remote machine instead, by clicking on the Launch Notebook icon available for each lecture\n", 78 | "\n", 79 | "![https://python.quantecon.org/_static/lecture_specific/troubleshooting/launch.png](https://python.quantecon.org/_static/lecture_specific/troubleshooting/launch.png)\n", 80 | "\n", 81 | "Second, you can report an issue, so we can try to fix your local set up.\n", 82 | "\n", 83 | "We like getting feedback on the lectures so please don’t hesitate to get in\n", 84 | "touch." 85 | ] 86 | }, 87 | { 88 | "cell_type": "markdown", 89 | "id": "c4058ffd", 90 | "metadata": {}, 91 | "source": [ 92 | "## Reporting an Issue\n", 93 | "\n", 94 | "One way to give feedback is to raise an issue through our [issue tracker](https://github.com/QuantEcon/lecture-python/issues).\n", 95 | "\n", 96 | "Please be as specific as possible. Tell us where the problem is and as much\n", 97 | "detail about your local set up as you can provide.\n", 98 | "\n", 99 | "Finally, you can provide direct feedback to [contact@quantecon.org](mailto:contact@quantecon.org)" 100 | ] 101 | } 102 | ], 103 | "metadata": { 104 | "date": 1764675755.943279, 105 | "filename": "troubleshooting.md", 106 | "kernelspec": { 107 | "display_name": "Python", 108 | "language": "python3", 109 | "name": "python3" 110 | }, 111 | "title": "Troubleshooting" 112 | }, 113 | "nbformat": 4, 114 | "nbformat_minor": 5 115 | } -------------------------------------------------------------------------------- /cross_product_trick.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "88bc9b49", 6 | "metadata": {}, 7 | "source": [ 8 | "# Eliminating Cross Products" 9 | ] 10 | }, 11 | { 12 | "cell_type": "markdown", 13 | "id": "36ea6247", 14 | "metadata": {}, 15 | "source": [ 16 | "## Overview\n", 17 | "\n", 18 | "This lecture describes formulas for eliminating\n", 19 | "\n", 20 | "- cross products between states and control in linear-quadratic dynamic programming problems \n", 21 | "- covariances between state and measurement noises in Kalman filtering problems \n", 22 | "\n", 23 | "\n", 24 | "For a linear-quadratic dynamic programming problem, the idea involves these steps\n", 25 | "\n", 26 | "- transform states and controls in a way that leads to an equivalent problem with no cross-products between transformed states and controls \n", 27 | "- solve the transformed problem using standard formulas for problems with no cross-products between states and controls presented in this lecture [Linear Control: Foundations](https://python.quantecon.org/lqcontrol.html) \n", 28 | "- transform the optimal decision rule for the altered problem into the optimal decision rule for the original problem with cross-products between states and controls " 29 | ] 30 | }, 31 | { 32 | "cell_type": "markdown", 33 | "id": "a35f2a1c", 34 | "metadata": {}, 35 | "source": [ 36 | "## Undiscounted Dynamic Programming Problem\n", 37 | "\n", 38 | "Here is a nonstochastic undiscounted LQ dynamic programming with cross products between\n", 39 | "states and controls in the objective function.\n", 40 | "\n", 41 | "The problem is defined by the 5-tuple of matrices $ (A, B, R, Q, H) $\n", 42 | "where $ R $ and $ Q $ are positive definite symmetric matrices and\n", 43 | "$ A \\sim m \\times m, B \\sim m \\times k, Q \\sim k \\times k, R \\sim m \\times m $ and $ H \\sim k \\times m $.\n", 44 | "\n", 45 | "The problem is to choose $ \\{x_{t+1}, u_t\\}_{t=0}^\\infty $ to maximize\n", 46 | "\n", 47 | "$$\n", 48 | "- \\sum_{t=0}^\\infty (x_t' R x_t + u_t' Q u_t + 2 u_t H x_t)\n", 49 | "$$\n", 50 | "\n", 51 | "subject to the linear constraints\n", 52 | "\n", 53 | "$$\n", 54 | "x_{t+1} = A x_t + B u_t, \\quad t \\geq 0\n", 55 | "$$\n", 56 | "\n", 57 | "where $ x_0 $ is a given initial condition.\n", 58 | "\n", 59 | "The solution to this undiscounted infinite-horizon problem is a time-invariant feedback rule\n", 60 | "\n", 61 | "$$\n", 62 | "u_t = -F x_t\n", 63 | "$$\n", 64 | "\n", 65 | "where\n", 66 | "\n", 67 | "$$\n", 68 | "F = -(Q + B'PB)^{-1} B'PA\n", 69 | "$$\n", 70 | "\n", 71 | "and $ P \\sim m \\times m $ is a positive definite solution of the algebraic matrix Riccati equation\n", 72 | "\n", 73 | "$$\n", 74 | "P = R + A'PA - (A'PB + H')(Q + B'PB)^{-1}(B'PA + H).\n", 75 | "$$\n", 76 | "\n", 77 | "It can be verified that an **equivalent** problem without cross-products between states and controls\n", 78 | "is defined by a 4-tuple of matrices : $ (A^*, B, R^*, Q) $.\n", 79 | "\n", 80 | "That the omitted matrix $ H=0 $ indicates that there are no cross products between states and controls\n", 81 | "in the equivalent problem.\n", 82 | "\n", 83 | "The matrices $ (A^*, B, R^*, Q) $ defining the equivalent problem and the value function, policy function matrices $ P, F^* $ that solve it are related to the matrices $ (A, B, R, Q, H) $ defining the original problem and the value function, policy function matrices $ P, F $ that solve the original problem by\n", 84 | "\n", 85 | "$$\n", 86 | "\\begin{align*}\n", 87 | "A^* & = A - B Q^{-1} H, \\\\\n", 88 | "R^* & = R - H'Q^{-1} H, \\\\\n", 89 | "P & = R^* + {A^*}' P A - ({A^*}' P B) (Q + B' P B)^{-1} B' P A^*, \\\\\n", 90 | "F^* & = (Q + B' P B)^{-1} B' P A^*, \\\\\n", 91 | "F & = F^* + Q^{-1} H.\n", 92 | "\\end{align*}\n", 93 | "$$" 94 | ] 95 | }, 96 | { 97 | "cell_type": "markdown", 98 | "id": "192a70b8", 99 | "metadata": {}, 100 | "source": [ 101 | "## Kalman Filter\n", 102 | "\n", 103 | "The **duality** that prevails between a linear-quadratic optimal control and a Kalman filtering problem means that there is an analogous transformation that allows us to transform a Kalman filtering problem\n", 104 | "with non-zero covariance matrix between between shocks to states and shocks to measurements to an equivalent Kalman filtering problem with zero covariance between shocks to states and measurments.\n", 105 | "\n", 106 | "Let’s look at the appropriate transformations.\n", 107 | "\n", 108 | "First, let’s recall the Kalman filter with covariance between noises to states and measurements.\n", 109 | "\n", 110 | "The hidden Markov model is\n", 111 | "\n", 112 | "$$\n", 113 | "\\begin{align*}\n", 114 | "x_{t+1} & = A x_t + B w_{t+1}, \\\\\n", 115 | "z_{t+1} & = D x_t + F w_{t+1}, \n", 116 | "\\end{align*}\n", 117 | "$$\n", 118 | "\n", 119 | "where $ A \\sim m \\times m, B \\sim m \\times p $ and $ D \\sim k \\times m, F \\sim k \\times p $,\n", 120 | "and $ w_{t+1} $ is the time $ t+1 $ component of a sequence of i.i.d. $ p \\times 1 $ normally distibuted\n", 121 | "random vectors with mean vector zero and covariance matrix equal to a $ p \\times p $ identity matrix.\n", 122 | "\n", 123 | "Thus, $ x_t $ is $ m \\times 1 $ and $ z_t $ is $ k \\times 1 $.\n", 124 | "\n", 125 | "The Kalman filtering formulas are\n", 126 | "\n", 127 | "$$\n", 128 | "\\begin{align*}\n", 129 | "K(\\Sigma_t) & = (A \\Sigma_t D' + BF')(D \\Sigma_t D' + FF')^{-1}, \\\\\n", 130 | "\\Sigma_{t+1}& = A \\Sigma_t A' + BB' - (A \\Sigma_t D' + BF')(D \\Sigma_t D' + FF')^{-1} (D \\Sigma_t A' + FB').\n", 131 | "\\end{align*} (eq:Kalman102)\n", 132 | " \n", 133 | "\n", 134 | "Define tranformed matrices\n", 135 | "\n", 136 | "\\begin{align*}\n", 137 | "A^* & = A - BF' (FF')^{-1} D, \\\\\n", 138 | "B^* {B^*}' & = BB' - BF' (FF')^{-1} FB'.\n", 139 | "\\end{align*}\n", 140 | "$$" 141 | ] 142 | }, 143 | { 144 | "cell_type": "markdown", 145 | "id": "5f2c5180", 146 | "metadata": {}, 147 | "source": [ 148 | "### Algorithm\n", 149 | "\n", 150 | "A consequence of formulas {eq}`eq:Kalman102} is that we can use the following algorithm to solve Kalman filtering problems that involve non zero covariances between state and signal noises.\n", 151 | "\n", 152 | "First, compute $ \\Sigma, K^* $ using the ordinary Kalman filtering formula with $ BF' = 0 $, i.e.,\n", 153 | "with zero covariance matrix between random shocks to states and random shocks to measurements.\n", 154 | "\n", 155 | "That is, compute $ K^* $ and $ \\Sigma $ that satisfy\n", 156 | "\n", 157 | "$$\n", 158 | "\\begin{align*}\n", 159 | "K^* & = (A^* \\Sigma D')(D \\Sigma D' + FF')^{-1} \\\\\n", 160 | "\\Sigma & = A^* \\Sigma {A^*}' + B^* {B^*}' - (A^* \\Sigma D')(D \\Sigma D' + FF')^{-1} (D \\Sigma {A^*}').\n", 161 | "\\end{align*}\n", 162 | "$$\n", 163 | "\n", 164 | "The Kalman gain for the original problem **with non-zero covariance** between shocks to states and measurements is then\n", 165 | "\n", 166 | "$$\n", 167 | "K = K^* + BF' (FF')^{-1},\n", 168 | "$$\n", 169 | "\n", 170 | "The state reconstruction covariance matrix $ \\Sigma $ for the original problem equals the state reconstrution covariance matrix for the transformed problem." 171 | ] 172 | }, 173 | { 174 | "cell_type": "markdown", 175 | "id": "352edf14", 176 | "metadata": {}, 177 | "source": [ 178 | "## Duality table\n", 179 | "\n", 180 | "Here is a handy table to remember how the Kalman filter and dynamic program are related.\n", 181 | "\n", 182 | "|Dynamic Program|Kalman Filter|\n", 183 | "|:------------------------------------------------:|:------------------------------------------------:|\n", 184 | "|$ A $|$ A' $|\n", 185 | "|$ B $|$ D' $|\n", 186 | "|$ H $|$ FB' $|\n", 187 | "|$ Q $|$ FF' $|\n", 188 | "|$ R $|$ BB' $|\n", 189 | "|$ F $|$ K' $|\n", 190 | "|$ P $|$ \\Sigma $|" 191 | ] 192 | } 193 | ], 194 | "metadata": { 195 | "date": 1764675748.3485882, 196 | "filename": "cross_product_trick.md", 197 | "kernelspec": { 198 | "display_name": "Python", 199 | "language": "python3", 200 | "name": "python3" 201 | }, 202 | "title": "Eliminating Cross Products" 203 | }, 204 | "nbformat": 4, 205 | "nbformat_minor": 5 206 | } -------------------------------------------------------------------------------- /intro.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "d6f6477b", 6 | "metadata": {}, 7 | "source": [ 8 | "# Intermediate Quantitative Economics with Python\n", 9 | "\n", 10 | "This website presents a set of lectures on quantitative economic modeling." 11 | ] 12 | }, 13 | { 14 | "cell_type": "markdown", 15 | "id": "f9c963b1", 16 | "metadata": {}, 17 | "source": [ 18 | "# Tools and Techniques\n", 19 | "\n", 20 | "- [Modeling COVID 19](https://python.quantecon.org/sir_model.html)\n", 21 | "- [Linear Algebra](https://python.quantecon.org/linear_algebra.html)\n", 22 | "- [QR Decomposition](https://python.quantecon.org/qr_decomp.html)\n", 23 | "- [Circulant Matrices](https://python.quantecon.org/eig_circulant.html)\n", 24 | "- [Singular Value Decomposition (SVD)](https://python.quantecon.org/svd_intro.html)\n", 25 | "- [VARs and DMDs](https://python.quantecon.org/var_dmd.html)\n", 26 | "- [Using Newton’s Method to Solve Economic Models](https://python.quantecon.org/newton_method.html)" 27 | ] 28 | }, 29 | { 30 | "cell_type": "markdown", 31 | "id": "811f0143", 32 | "metadata": {}, 33 | "source": [ 34 | "# Elementary Statistics\n", 35 | "\n", 36 | "- [Elementary Probability with Matrices](https://python.quantecon.org/prob_matrix.html)\n", 37 | "- [Some Probability Distributions](https://python.quantecon.org/stats_examples.html)\n", 38 | "- [LLN and CLT](https://python.quantecon.org/lln_clt.html)\n", 39 | "- [Two Meanings of Probability](https://python.quantecon.org/prob_meaning.html)\n", 40 | "- [Multivariate Hypergeometric Distribution](https://python.quantecon.org/multi_hyper.html)\n", 41 | "- [Multivariate Normal Distribution](https://python.quantecon.org/multivariate_normal.html)\n", 42 | "- [Fault Tree Uncertainties](https://python.quantecon.org/hoist_failure.html)\n", 43 | "- [Introduction to Artificial Neural Networks](https://python.quantecon.org/back_prop.html)\n", 44 | "- [Randomized Response Surveys](https://python.quantecon.org/rand_resp.html)\n", 45 | "- [Expected Utilities of Random Responses](https://python.quantecon.org/util_rand_resp.html)" 46 | ] 47 | }, 48 | { 49 | "cell_type": "markdown", 50 | "id": "d731ce95", 51 | "metadata": {}, 52 | "source": [ 53 | "# Bayes Law\n", 54 | "\n", 55 | "- [Non-Conjugate Priors](https://python.quantecon.org/bayes_nonconj.html)\n", 56 | "- [Posterior Distributions for AR(1) Parameters](https://python.quantecon.org/ar1_bayes.html)\n", 57 | "- [Forecasting an AR(1) Process](https://python.quantecon.org/ar1_turningpts.html)" 58 | ] 59 | }, 60 | { 61 | "cell_type": "markdown", 62 | "id": "2771f4fa", 63 | "metadata": {}, 64 | "source": [ 65 | "# Statistics and Information\n", 66 | "\n", 67 | "- [Statistical Divergence Measures](https://python.quantecon.org/divergence_measures.html)\n", 68 | "- [Likelihood Ratio Processes](https://python.quantecon.org/likelihood_ratio_process.html)\n", 69 | "- [Heterogeneous Beliefs and Financial Markets](https://python.quantecon.org/likelihood_ratio_process_2.html)\n", 70 | "- [Likelihood Processes For VAR Models](https://python.quantecon.org/likelihood_var.html)\n", 71 | "- [Mean of a Likelihood Ratio Process](https://python.quantecon.org/imp_sample.html)\n", 72 | "- [A Problem that Stumped Milton Friedman](https://python.quantecon.org/wald_friedman.html)\n", 73 | "- [A Bayesian Formulation of Friedman and Wald’s Problem](https://python.quantecon.org/wald_friedman_2.html)\n", 74 | "- [Exchangeability and Bayesian Updating](https://python.quantecon.org/exchangeable.html)\n", 75 | "- [Likelihood Ratio Processes and Bayesian Learning](https://python.quantecon.org/likelihood_bayes.html)\n", 76 | "- [Incorrect Models](https://python.quantecon.org/mix_model.html)\n", 77 | "- [Bayesian versus Frequentist Decision Rules](https://python.quantecon.org/navy_captain.html)" 78 | ] 79 | }, 80 | { 81 | "cell_type": "markdown", 82 | "id": "36bd9b3e", 83 | "metadata": {}, 84 | "source": [ 85 | "# Linear Programming\n", 86 | "\n", 87 | "- [Optimal Transport](https://python.quantecon.org/opt_transport.html)\n", 88 | "- [Von Neumann Growth Model (and a Generalization)](https://python.quantecon.org/von_neumann_model.html)" 89 | ] 90 | }, 91 | { 92 | "cell_type": "markdown", 93 | "id": "aea428e3", 94 | "metadata": {}, 95 | "source": [ 96 | "# Introduction to Dynamics\n", 97 | "\n", 98 | "- [Finite Markov Chains](https://python.quantecon.org/finite_markov.html)\n", 99 | "- [Inventory Dynamics](https://python.quantecon.org/inventory_dynamics.html)\n", 100 | "- [Linear State Space Models](https://python.quantecon.org/linear_models.html)\n", 101 | "- [Samuelson Multiplier-Accelerator](https://python.quantecon.org/samuelson.html)\n", 102 | "- [Kesten Processes and Firm Dynamics](https://python.quantecon.org/kesten_processes.html)\n", 103 | "- [Wealth Distribution Dynamics](https://python.quantecon.org/wealth_dynamics.html)\n", 104 | "- [A First Look at the Kalman Filter](https://python.quantecon.org/kalman.html)\n", 105 | "- [Another Look at the Kalman Filter](https://python.quantecon.org/kalman_2.html)" 106 | ] 107 | }, 108 | { 109 | "cell_type": "markdown", 110 | "id": "da56d2f9", 111 | "metadata": {}, 112 | "source": [ 113 | "# Search\n", 114 | "\n", 115 | "- [Job Search I: The McCall Search Model](https://python.quantecon.org/mccall_model.html)\n", 116 | "- [Job Search II: Search and Separation](https://python.quantecon.org/mccall_model_with_separation.html)\n", 117 | "- [Job Search III: Search with Separation and Markov Wages](https://python.quantecon.org/mccall_model_with_sep_markov.html)\n", 118 | "- [Job Search IV: Fitted Value Function Iteration](https://python.quantecon.org/mccall_fitted_vfi.html)\n", 119 | "- [Job Search V: Persistent and Transitory Wage Shocks](https://python.quantecon.org/mccall_persist_trans.html)\n", 120 | "- [Job Search VI: Modeling Career Choice](https://python.quantecon.org/career.html)\n", 121 | "- [Job Search VII: On-the-Job Search](https://python.quantecon.org/jv.html)\n", 122 | "- [Job Search VIII: Search with Learning](https://python.quantecon.org/odu.html)\n", 123 | "- [Job Search IX: Search with Q-Learning](https://python.quantecon.org/mccall_q.html)" 124 | ] 125 | }, 126 | { 127 | "cell_type": "markdown", 128 | "id": "8858aa3f", 129 | "metadata": {}, 130 | "source": [ 131 | "# Introduction to Optimal Savings\n", 132 | "\n", 133 | "- [Optimal Savings I: Cake Eating](https://python.quantecon.org/os.html)\n", 134 | "- [Optimal Savings II: Numerical Cake Eating](https://python.quantecon.org/os_numerical.html)\n", 135 | "- [Optimal Savings III: Stochastic Returns](https://python.quantecon.org/os_stochastic.html)\n", 136 | "- [Optimal Savings IV: Time Iteration](https://python.quantecon.org/os_time_iter.html)\n", 137 | "- [Optimal Savings V: The Endogenous Grid Method](https://python.quantecon.org/os_egm.html)\n", 138 | "- [Optimal Savings VI: EGM with JAX](https://python.quantecon.org/os_egm_jax.html)" 139 | ] 140 | }, 141 | { 142 | "cell_type": "markdown", 143 | "id": "c506a3db", 144 | "metadata": {}, 145 | "source": [ 146 | "# Household Problems\n", 147 | "\n", 148 | "- [The Income Fluctuation Problem I: Discretization and VFI](https://python.quantecon.org/ifp_discrete.html)\n", 149 | "- [The Income Fluctuation Problem II: Optimistic Policy Iteration](https://python.quantecon.org/ifp_opi.html)\n", 150 | "- [The Income Fluctuation Problem III: The Endogenous Grid Method](https://python.quantecon.org/ifp_egm.html)\n", 151 | "- [The Income Fluctuation Problem IV: Transient Income Shocks](https://python.quantecon.org/ifp_egm_transient_shocks.html)\n", 152 | "- [The Income Fluctuation Problem V: Stochastic Returns on Assets](https://python.quantecon.org/ifp_advanced.html)" 153 | ] 154 | }, 155 | { 156 | "cell_type": "markdown", 157 | "id": "83e4bde8", 158 | "metadata": {}, 159 | "source": [ 160 | "# LQ Control\n", 161 | "\n", 162 | "- [LQ Control: Foundations](https://python.quantecon.org/lqcontrol.html)\n", 163 | "- [Lagrangian for LQ Control](https://python.quantecon.org/lagrangian_lqdp.html)\n", 164 | "- [Eliminating Cross Products](https://python.quantecon.org/cross_product_trick.html)\n", 165 | "- [The Permanent Income Model](https://python.quantecon.org/perm_income.html)\n", 166 | "- [Permanent Income II: LQ Techniques](https://python.quantecon.org/perm_income_cons.html)\n", 167 | "- [Production Smoothing via Inventories](https://python.quantecon.org/lq_inventories.html)" 168 | ] 169 | }, 170 | { 171 | "cell_type": "markdown", 172 | "id": "19452c8c", 173 | "metadata": {}, 174 | "source": [ 175 | "# Optimal Growth\n", 176 | "\n", 177 | "- [Cass-Koopmans Model](https://python.quantecon.org/cass_koopmans_1.html)\n", 178 | "- [Cass-Koopmans Competitive Equilibrium](https://python.quantecon.org/cass_koopmans_2.html)\n", 179 | "- [Cass-Koopmans Model with Distorting Taxes](https://python.quantecon.org/cass_fiscal.html)\n", 180 | "- [Two-Country Model with Distorting Taxes](https://python.quantecon.org/cass_fiscal_2.html)\n", 181 | "- [Transitions in an Overlapping Generations Model](https://python.quantecon.org/ak2.html)" 182 | ] 183 | }, 184 | { 185 | "cell_type": "markdown", 186 | "id": "f5d387f9", 187 | "metadata": {}, 188 | "source": [ 189 | "# Multiple Agent Models\n", 190 | "\n", 191 | "- [A Lake Model of Employment and Unemployment](https://python.quantecon.org/lake_model.html)\n", 192 | "- [Lake Model with an Endogenous Job Finding Rate](https://python.quantecon.org/endogenous_lake.html)\n", 193 | "- [Rational Expectations Equilibrium](https://python.quantecon.org/rational_expectations.html)\n", 194 | "- [Stability in Linear Rational Expectations Models](https://python.quantecon.org/re_with_feedback.html)\n", 195 | "- [Markov Perfect Equilibrium](https://python.quantecon.org/markov_perf.html)\n", 196 | "- [Uncertainty Traps](https://python.quantecon.org/uncertainty_traps.html)\n", 197 | "- [The Aiyagari Model](https://python.quantecon.org/aiyagari.html)\n", 198 | "- [A Long-Lived, Heterogeneous Agent, Overlapping Generations Model](https://python.quantecon.org/ak_aiyagari.html)" 199 | ] 200 | }, 201 | { 202 | "cell_type": "markdown", 203 | "id": "4e55c1e4", 204 | "metadata": {}, 205 | "source": [ 206 | "# Asset Pricing and Finance\n", 207 | "\n", 208 | "- [Asset Pricing: Finite State Models](https://python.quantecon.org/markov_asset.html)\n", 209 | "- [Competitive Equilibria with Arrow Securities](https://python.quantecon.org/ge_arrow.html)\n", 210 | "- [Heterogeneous Beliefs and Bubbles](https://python.quantecon.org/harrison_kreps.html)\n", 211 | "- [Speculative Behavior with Bayesian Learning](https://python.quantecon.org/morris_learn.html)" 212 | ] 213 | }, 214 | { 215 | "cell_type": "markdown", 216 | "id": "a639d899", 217 | "metadata": {}, 218 | "source": [ 219 | "# Data and Empirics\n", 220 | "\n", 221 | "- [Pandas for Panel Data](https://python.quantecon.org/pandas_panel.html)\n", 222 | "- [Linear Regression in Python](https://python.quantecon.org/ols.html)\n", 223 | "- [Maximum Likelihood Estimation](https://python.quantecon.org/mle.html)" 224 | ] 225 | }, 226 | { 227 | "cell_type": "markdown", 228 | "id": "fed79450", 229 | "metadata": {}, 230 | "source": [ 231 | "# Auctions\n", 232 | "\n", 233 | "- [First-Price and Second-Price Auctions](https://python.quantecon.org/two_auctions.html)\n", 234 | "- [Multiple Good Allocation Mechanisms](https://python.quantecon.org/house_auction.html)" 235 | ] 236 | }, 237 | { 238 | "cell_type": "markdown", 239 | "id": "d4734599", 240 | "metadata": {}, 241 | "source": [ 242 | "# Other\n", 243 | "\n", 244 | "- [Troubleshooting](https://python.quantecon.org/troubleshooting.html)\n", 245 | "- [References](https://python.quantecon.org/zreferences.html)\n", 246 | "- [Execution Statistics](https://python.quantecon.org/status.html)" 247 | ] 248 | } 249 | ], 250 | "metadata": { 251 | "date": 1764675750.4240167, 252 | "filename": "intro.md", 253 | "kernelspec": { 254 | "display_name": "Python", 255 | "language": "python3", 256 | "name": "python3" 257 | }, 258 | "title": "Intermediate Quantitative Economics with Python" 259 | }, 260 | "nbformat": 4, 261 | "nbformat_minor": 5 262 | } -------------------------------------------------------------------------------- /status.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "1335d5ad", 6 | "metadata": {}, 7 | "source": [ 8 | "# Execution Statistics\n", 9 | "\n", 10 | "This table contains the latest execution statistics.\n", 11 | "\n", 12 | "[](https://python.quantecon.org/aiyagari.html)[](https://python.quantecon.org/ak2.html)[](https://python.quantecon.org/ak_aiyagari.html)[](https://python.quantecon.org/ar1_bayes.html)[](https://python.quantecon.org/ar1_turningpts.html)[](https://python.quantecon.org/back_prop.html)[](https://python.quantecon.org/bayes_nonconj.html)[](https://python.quantecon.org/career.html)[](https://python.quantecon.org/cass_fiscal.html)[](https://python.quantecon.org/cass_fiscal_2.html)[](https://python.quantecon.org/cass_koopmans_1.html)[](https://python.quantecon.org/cass_koopmans_2.html)[](https://python.quantecon.org/cross_product_trick.html)[](https://python.quantecon.org/divergence_measures.html)[](https://python.quantecon.org/eig_circulant.html)[](https://python.quantecon.org/endogenous_lake.html)[](https://python.quantecon.org/exchangeable.html)[](https://python.quantecon.org/finite_markov.html)[](https://python.quantecon.org/ge_arrow.html)[](https://python.quantecon.org/harrison_kreps.html)[](https://python.quantecon.org/hoist_failure.html)[](https://python.quantecon.org/house_auction.html)[](https://python.quantecon.org/ifp_advanced.html)[](https://python.quantecon.org/ifp_discrete.html)[](https://python.quantecon.org/ifp_egm.html)[](https://python.quantecon.org/ifp_egm_transient_shocks.html)[](https://python.quantecon.org/ifp_opi.html)[](https://python.quantecon.org/imp_sample.html)[](https://python.quantecon.org/intro.html)[](https://python.quantecon.org/inventory_dynamics.html)[](https://python.quantecon.org/jv.html)[](https://python.quantecon.org/kalman.html)[](https://python.quantecon.org/kalman_2.html)[](https://python.quantecon.org/kesten_processes.html)[](https://python.quantecon.org/lagrangian_lqdp.html)[](https://python.quantecon.org/lake_model.html)[](https://python.quantecon.org/likelihood_bayes.html)[](https://python.quantecon.org/likelihood_ratio_process.html)[](https://python.quantecon.org/likelihood_ratio_process_2.html)[](https://python.quantecon.org/likelihood_var.html)[](https://python.quantecon.org/linear_algebra.html)[](https://python.quantecon.org/linear_models.html)[](https://python.quantecon.org/lln_clt.html)[](https://python.quantecon.org/lq_inventories.html)[](https://python.quantecon.org/lqcontrol.html)[](https://python.quantecon.org/markov_asset.html)[](https://python.quantecon.org/markov_perf.html)[](https://python.quantecon.org/mccall_fitted_vfi.html)[](https://python.quantecon.org/mccall_model.html)[](https://python.quantecon.org/mccall_model_with_sep_markov.html)[](https://python.quantecon.org/mccall_model_with_separation.html)[](https://python.quantecon.org/mccall_persist_trans.html)[](https://python.quantecon.org/mccall_q.html)[](https://python.quantecon.org/mix_model.html)[](https://python.quantecon.org/mle.html)[](https://python.quantecon.org/morris_learn.html)[](https://python.quantecon.org/multi_hyper.html)[](https://python.quantecon.org/multivariate_normal.html)[](https://python.quantecon.org/navy_captain.html)[](https://python.quantecon.org/newton_method.html)[](https://python.quantecon.org/odu.html)[](https://python.quantecon.org/ols.html)[](https://python.quantecon.org/opt_transport.html)[](https://python.quantecon.org/os.html)[](https://python.quantecon.org/os_egm.html)[](https://python.quantecon.org/os_egm_jax.html)[](https://python.quantecon.org/os_numerical.html)[](https://python.quantecon.org/os_stochastic.html)[](https://python.quantecon.org/os_time_iter.html)[](https://python.quantecon.org/pandas_panel.html)[](https://python.quantecon.org/perm_income.html)[](https://python.quantecon.org/perm_income_cons.html)[](https://python.quantecon.org/prob_matrix.html)[](https://python.quantecon.org/prob_meaning.html)[](https://python.quantecon.org/qr_decomp.html)[](https://python.quantecon.org/rand_resp.html)[](https://python.quantecon.org/rational_expectations.html)[](https://python.quantecon.org/re_with_feedback.html)[](https://python.quantecon.org/samuelson.html)[](https://python.quantecon.org/sir_model.html)[](https://python.quantecon.org/stats_examples.html)[](https://python.quantecon.org/.html)[](https://python.quantecon.org/svd_intro.html)[](https://python.quantecon.org/troubleshooting.html)[](https://python.quantecon.org/two_auctions.html)[](https://python.quantecon.org/uncertainty_traps.html)[](https://python.quantecon.org/util_rand_resp.html)[](https://python.quantecon.org/var_dmd.html)[](https://python.quantecon.org/von_neumann_model.html)[](https://python.quantecon.org/wald_friedman.html)[](https://python.quantecon.org/wald_friedman_2.html)[](https://python.quantecon.org/wealth_dynamics.html)[](https://python.quantecon.org/zreferences.html)|Document|Modified|Method|Run Time (s)|Status|\n", 13 | "|:------------------:|:------------------:|:------------------:|:------------------:|:------------------:|\n", 14 | "|aiyagari|2025-12-01 05:34|cache|23.44|✅|\n", 15 | "|ak2|2025-12-01 05:34|cache|11.49|✅|\n", 16 | "|ak_aiyagari|2025-12-01 05:34|cache|29.35|✅|\n", 17 | "|ar1_bayes|2025-12-01 05:40|cache|319.49|✅|\n", 18 | "|ar1_turningpts|2025-12-01 05:40|cache|35.86|✅|\n", 19 | "|back_prop|2025-12-01 05:42|cache|92.74|✅|\n", 20 | "|bayes_nonconj|2025-12-01 05:59|cache|1059.73|✅|\n", 21 | "|career|2025-12-01 06:00|cache|11.41|✅|\n", 22 | "|cass_fiscal|2025-12-01 06:00|cache|16.91|✅|\n", 23 | "|cass_fiscal_2|2025-12-01 06:00|cache|5.57|✅|\n", 24 | "|cass_koopmans_1|2025-12-01 06:00|cache|12.55|✅|\n", 25 | "|cass_koopmans_2|2025-12-01 06:00|cache|6.48|✅|\n", 26 | "|cross_product_trick|2025-12-01 06:00|cache|0.91|✅|\n", 27 | "|divergence_measures|2025-12-01 06:01|cache|12.16|✅|\n", 28 | "|eig_circulant|2025-12-01 06:01|cache|6.19|✅|\n", 29 | "|endogenous_lake|2025-12-01 06:09|cache|475.98|✅|\n", 30 | "|exchangeable|2025-12-01 06:09|cache|7.31|✅|\n", 31 | "|finite_markov|2025-12-01 06:09|cache|5.94|✅|\n", 32 | "|ge_arrow|2025-12-01 06:09|cache|1.92|✅|\n", 33 | "|harrison_kreps|2025-12-01 06:09|cache|3.46|✅|\n", 34 | "|hoist_failure|2025-12-01 06:10|cache|45.62|✅|\n", 35 | "|house_auction|2025-12-01 06:10|cache|3.02|✅|\n", 36 | "|ifp_advanced|2025-12-02 11:35|cache|36.51|✅|\n", 37 | "|ifp_discrete|2025-12-01 06:10|cache|13.09|✅|\n", 38 | "|ifp_egm|2025-12-01 06:10|cache|9.69|✅|\n", 39 | "|ifp_egm_transient_shocks|2025-12-02 11:35|cache|17.18|✅|\n", 40 | "|ifp_opi|2025-12-01 06:11|cache|8.76|✅|\n", 41 | "|imp_sample|2025-12-01 06:15|cache|243.23|✅|\n", 42 | "|intro|2025-12-01 06:15|cache|0.97|✅|\n", 43 | "|inventory_dynamics|2025-12-01 06:15|cache|9.39|✅|\n", 44 | "|jv|2025-12-01 06:15|cache|14.64|✅|\n", 45 | "|kalman|2025-12-01 06:16|cache|8.14|✅|\n", 46 | "|kalman_2|2025-12-01 06:16|cache|42.85|✅|\n", 47 | "|kesten_processes|2025-12-01 06:17|cache|29.92|✅|\n", 48 | "|lagrangian_lqdp|2025-12-01 06:17|cache|24.68|✅|\n", 49 | "|lake_model|2025-12-01 06:17|cache|10.2|✅|\n", 50 | "|likelihood_bayes|2025-12-01 06:18|cache|41.78|✅|\n", 51 | "|likelihood_ratio_process|2025-12-01 06:18|cache|20.94|✅|\n", 52 | "|likelihood_ratio_process_2|2025-12-01 06:19|cache|28.17|✅|\n", 53 | "|likelihood_var|2025-12-01 06:19|cache|18.38|✅|\n", 54 | "|linear_algebra|2025-12-01 06:19|cache|2.37|✅|\n", 55 | "|linear_models|2025-12-01 06:19|cache|8.21|✅|\n", 56 | "|lln_clt|2025-12-01 06:20|cache|9.63|✅|\n", 57 | "|lq_inventories|2025-12-01 06:20|cache|11.4|✅|\n", 58 | "|lqcontrol|2025-12-01 06:20|cache|4.58|✅|\n", 59 | "|markov_asset|2025-12-01 06:20|cache|4.88|✅|\n", 60 | "|markov_perf|2025-12-01 06:20|cache|4.32|✅|\n", 61 | "|mccall_fitted_vfi|2025-12-01 06:21|cache|42.15|✅|\n", 62 | "|mccall_model|2025-12-01 06:21|cache|14.16|✅|\n", 63 | "|mccall_model_with_sep_markov|2025-12-01 06:21|cache|23.66|✅|\n", 64 | "|mccall_model_with_separation|2025-12-01 06:22|cache|13.02|✅|\n", 65 | "|mccall_persist_trans|2025-12-01 06:22|cache|11.04|✅|\n", 66 | "|mccall_q|2025-12-01 06:22|cache|17.26|✅|\n", 67 | "|mix_model|2025-12-01 06:23|cache|73.37|✅|\n", 68 | "|mle|2025-12-01 06:23|cache|10.68|✅|\n", 69 | "|morris_learn|2025-12-01 06:24|cache|15.54|✅|\n", 70 | "|multi_hyper|2025-12-01 06:24|cache|16.81|✅|\n", 71 | "|multivariate_normal|2025-12-01 06:24|cache|4.05|✅|\n", 72 | "|navy_captain|2025-12-01 06:24|cache|28.09|✅|\n", 73 | "|newton_method|2025-12-01 06:25|cache|55.42|✅|\n", 74 | "|odu|2025-12-01 06:26|cache|47.01|✅|\n", 75 | "|ols|2025-12-01 06:26|cache|8.04|✅|\n", 76 | "|opt_transport|2025-12-01 06:27|cache|11.95|✅|\n", 77 | "|os|2025-12-01 06:27|cache|1.75|✅|\n", 78 | "|os_egm|2025-12-02 11:35|cache|2.54|✅|\n", 79 | "|os_egm_jax|2025-12-02 11:35|cache|5.3|✅|\n", 80 | "|os_numerical|2025-12-02 11:36|cache|20.37|✅|\n", 81 | "|os_stochastic|2025-12-02 11:37|cache|56.18|✅|\n", 82 | "|os_time_iter|2025-12-01 06:28|cache|4.72|✅|\n", 83 | "|pandas_panel|2025-12-01 06:28|cache|4.45|✅|\n", 84 | "|perm_income|2025-12-01 06:28|cache|3.54|✅|\n", 85 | "|perm_income_cons|2025-12-01 06:28|cache|8.07|✅|\n", 86 | "|prob_matrix|2025-12-01 06:28|cache|9.35|✅|\n", 87 | "|prob_meaning|2025-12-01 06:29|cache|52.7|✅|\n", 88 | "|qr_decomp|2025-12-01 06:29|cache|1.28|✅|\n", 89 | "|rand_resp|2025-12-01 06:29|cache|2.5|✅|\n", 90 | "|rational_expectations|2025-12-01 06:29|cache|3.79|✅|\n", 91 | "|re_with_feedback|2025-12-01 06:30|cache|10.74|✅|\n", 92 | "|samuelson|2025-12-01 06:30|cache|11.15|✅|\n", 93 | "|sir_model|2025-12-01 06:30|cache|2.84|✅|\n", 94 | "|stats_examples|2025-12-01 06:30|cache|4.13|✅|\n", 95 | "|status|2025-12-01 06:30|cache|6.83|✅|\n", 96 | "|svd_intro|2025-12-01 06:30|cache|1.54|✅|\n", 97 | "|troubleshooting|2025-12-01 06:15|cache|0.97|✅|\n", 98 | "|two_auctions|2025-12-01 06:31|cache|23.77|✅|\n", 99 | "|uncertainty_traps|2025-12-01 06:31|cache|2.55|✅|\n", 100 | "|util_rand_resp|2025-12-01 06:31|cache|2.79|✅|\n", 101 | "|var_dmd|2025-12-01 06:15|cache|0.97|✅|\n", 102 | "|von_neumann_model|2025-12-01 06:31|cache|2.69|✅|\n", 103 | "|wald_friedman|2025-12-01 06:31|cache|19.07|✅|\n", 104 | "|wald_friedman_2|2025-12-01 06:31|cache|13.27|✅|\n", 105 | "|wealth_dynamics|2025-12-01 06:32|cache|30.76|✅|\n", 106 | "|zreferences|2025-12-01 06:15|cache|0.97|✅|\n", 107 | "\n", 108 | "\n", 109 | "These lectures are built on `linux` instances through `github actions`.\n", 110 | "\n", 111 | "These lectures are using the following python version" 112 | ] 113 | }, 114 | { 115 | "cell_type": "code", 116 | "execution_count": null, 117 | "id": "4624dee6", 118 | "metadata": { 119 | "hide-output": false 120 | }, 121 | "outputs": [], 122 | "source": [ 123 | "!python --version" 124 | ] 125 | }, 126 | { 127 | "cell_type": "markdown", 128 | "id": "28a623f4", 129 | "metadata": {}, 130 | "source": [ 131 | "and the following package versions" 132 | ] 133 | }, 134 | { 135 | "cell_type": "code", 136 | "execution_count": null, 137 | "id": "63a716a3", 138 | "metadata": { 139 | "hide-output": false 140 | }, 141 | "outputs": [], 142 | "source": [ 143 | "!conda list" 144 | ] 145 | }, 146 | { 147 | "cell_type": "markdown", 148 | "id": "5f14acc4", 149 | "metadata": {}, 150 | "source": [ 151 | "This lecture series has access to the following GPU" 152 | ] 153 | }, 154 | { 155 | "cell_type": "code", 156 | "execution_count": null, 157 | "id": "f97d09f8", 158 | "metadata": { 159 | "hide-output": false 160 | }, 161 | "outputs": [], 162 | "source": [ 163 | "!nvidia-smi" 164 | ] 165 | }, 166 | { 167 | "cell_type": "markdown", 168 | "id": "4b7599b7", 169 | "metadata": {}, 170 | "source": [ 171 | "You can check the backend used by JAX using:" 172 | ] 173 | }, 174 | { 175 | "cell_type": "code", 176 | "execution_count": null, 177 | "id": "59c95a5d", 178 | "metadata": { 179 | "hide-output": false 180 | }, 181 | "outputs": [], 182 | "source": [ 183 | "import jax\n", 184 | "# Check if JAX is using GPU\n", 185 | "print(f\"JAX backend: {jax.devices()[0].platform}\")" 186 | ] 187 | } 188 | ], 189 | "metadata": { 190 | "date": 1764675755.8923905, 191 | "filename": "status.md", 192 | "kernelspec": { 193 | "display_name": "Python", 194 | "language": "python3", 195 | "name": "python3" 196 | }, 197 | "title": "Execution Statistics" 198 | }, 199 | "nbformat": 4, 200 | "nbformat_minor": 5 201 | } -------------------------------------------------------------------------------- /schelling.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "cac95f22", 6 | "metadata": {}, 7 | "source": [ 8 | "\n", 9 | "" 10 | ] 11 | }, 12 | { 13 | "cell_type": "markdown", 14 | "id": "3b7537f0", 15 | "metadata": {}, 16 | "source": [ 17 | "# Schelling’s Segregation Model\n", 18 | "\n", 19 | "\n", 20 | "" 21 | ] 22 | }, 23 | { 24 | "cell_type": "markdown", 25 | "id": "b8369ce6", 26 | "metadata": {}, 27 | "source": [ 28 | "## Contents\n", 29 | "\n", 30 | "- [Schelling’s Segregation Model](#Schelling’s-Segregation-Model) \n", 31 | " - [Outline](#Outline) \n", 32 | " - [The Model](#The-Model) \n", 33 | " - [Results](#Results) \n", 34 | " - [Exercises](#Exercises) " 35 | ] 36 | }, 37 | { 38 | "cell_type": "markdown", 39 | "id": "55c28a5d", 40 | "metadata": {}, 41 | "source": [ 42 | "## Outline\n", 43 | "\n", 44 | "In 1969, Thomas C. Schelling developed a simple but striking model of racial segregation [[Schelling, 1969](https://python.quantecon.org/zreferences.html#id211)].\n", 45 | "\n", 46 | "His model studies the dynamics of racially mixed neighborhoods.\n", 47 | "\n", 48 | "Like much of Schelling’s work, the model shows how local interactions can lead to surprising aggregate structure.\n", 49 | "\n", 50 | "In particular, it shows that relatively mild preference for neighbors of similar race can lead in aggregate to the collapse of mixed neighborhoods, and high levels of segregation.\n", 51 | "\n", 52 | "In recognition of this and other research, Schelling was awarded the 2005 Nobel Prize in Economic Sciences (joint with Robert Aumann).\n", 53 | "\n", 54 | "In this lecture, we (in fact you) will build and run a version of Schelling’s model.\n", 55 | "\n", 56 | "Let’s start with some imports:" 57 | ] 58 | }, 59 | { 60 | "cell_type": "code", 61 | "execution_count": null, 62 | "id": "f3a1e4c5", 63 | "metadata": { 64 | "hide-output": false 65 | }, 66 | "outputs": [], 67 | "source": [ 68 | "import matplotlib.pyplot as plt\n", 69 | "plt.rcParams[\"figure.figsize\"] = (11, 5) #set default figure size\n", 70 | "from random import uniform, seed\n", 71 | "from math import sqrt" 72 | ] 73 | }, 74 | { 75 | "cell_type": "markdown", 76 | "id": "78e2f822", 77 | "metadata": {}, 78 | "source": [ 79 | "## The Model\n", 80 | "\n", 81 | "We will cover a variation of Schelling’s model that is easy to program and captures the main idea." 82 | ] 83 | }, 84 | { 85 | "cell_type": "markdown", 86 | "id": "9f330ebb", 87 | "metadata": {}, 88 | "source": [ 89 | "### Set-Up\n", 90 | "\n", 91 | "Suppose we have two types of people: orange people and green people.\n", 92 | "\n", 93 | "For the purpose of this lecture, we will assume there are 250 of each type.\n", 94 | "\n", 95 | "These agents all live on a single unit square.\n", 96 | "\n", 97 | "The location of an agent is just a point $ (x, y) $, where $ 0 < x, y < 1 $." 98 | ] 99 | }, 100 | { 101 | "cell_type": "markdown", 102 | "id": "7a4d81df", 103 | "metadata": {}, 104 | "source": [ 105 | "### Preferences\n", 106 | "\n", 107 | "We will say that an agent is *happy* if half or more of her 10 nearest neighbors are of the same type.\n", 108 | "\n", 109 | "Here ‘nearest’ is in terms of [Euclidean distance](https://en.wikipedia.org/wiki/Euclidean_distance).\n", 110 | "\n", 111 | "An agent who is not happy is called *unhappy*.\n", 112 | "\n", 113 | "An important point here is that agents are not averse to living in mixed areas.\n", 114 | "\n", 115 | "They are perfectly happy if half their neighbors are of the other color." 116 | ] 117 | }, 118 | { 119 | "cell_type": "markdown", 120 | "id": "7a0d8c73", 121 | "metadata": {}, 122 | "source": [ 123 | "### Behavior\n", 124 | "\n", 125 | "Initially, agents are mixed together (integrated).\n", 126 | "\n", 127 | "In particular, the initial location of each agent is an independent draw from a bivariate uniform distribution on $ S = (0, 1)^2 $.\n", 128 | "\n", 129 | "Now, cycling through the set of all agents, each agent is now given the chance to stay or move.\n", 130 | "\n", 131 | "We assume that each agent will stay put if they are happy and move if unhappy.\n", 132 | "\n", 133 | "The algorithm for moving is as follows\n", 134 | "\n", 135 | "1. Draw a random location in $ S $ \n", 136 | "1. If happy at new location, move there \n", 137 | "1. Else, go to step 1 \n", 138 | "\n", 139 | "\n", 140 | "In this way, we cycle continuously through the agents, moving as required.\n", 141 | "\n", 142 | "We continue to cycle until no one wishes to move." 143 | ] 144 | }, 145 | { 146 | "cell_type": "markdown", 147 | "id": "7683cf27", 148 | "metadata": {}, 149 | "source": [ 150 | "## Results\n", 151 | "\n", 152 | "Let’s have a look at the results we got when we coded and ran this model.\n", 153 | "\n", 154 | "As discussed above, agents are initially mixed randomly together.\n", 155 | "\n", 156 | "![https://python.quantecon.org/_static/lecture_specific/schelling/schelling_fig1.png](https://python.quantecon.org/_static/lecture_specific/schelling/schelling_fig1.png)\n", 157 | "\n", 158 | " \n", 159 | "But after several cycles, they become segregated into distinct regions.\n", 160 | "\n", 161 | "![https://python.quantecon.org/_static/lecture_specific/schelling/schelling_fig2.png](https://python.quantecon.org/_static/lecture_specific/schelling/schelling_fig2.png)\n", 162 | "\n", 163 | " \n", 164 | "![https://python.quantecon.org/_static/lecture_specific/schelling/schelling_fig3.png](https://python.quantecon.org/_static/lecture_specific/schelling/schelling_fig3.png)\n", 165 | "\n", 166 | " \n", 167 | "![https://python.quantecon.org/_static/lecture_specific/schelling/schelling_fig4.png](https://python.quantecon.org/_static/lecture_specific/schelling/schelling_fig4.png)\n", 168 | "\n", 169 | " \n", 170 | "In this instance, the program terminated after 4 cycles through the set of\n", 171 | "agents, indicating that all agents had reached a state of happiness.\n", 172 | "\n", 173 | "What is striking about the pictures is how rapidly racial integration breaks down.\n", 174 | "\n", 175 | "This is despite the fact that people in the model don’t actually mind living mixed with the other type.\n", 176 | "\n", 177 | "Even with these preferences, the outcome is a high degree of segregation." 178 | ] 179 | }, 180 | { 181 | "cell_type": "markdown", 182 | "id": "377ec057", 183 | "metadata": {}, 184 | "source": [ 185 | "## Exercises" 186 | ] 187 | }, 188 | { 189 | "cell_type": "markdown", 190 | "id": "62072708", 191 | "metadata": {}, 192 | "source": [ 193 | "## Exercise 67.1\n", 194 | "\n", 195 | "Implement and run this simulation for yourself.\n", 196 | "\n", 197 | "Consider the following structure for your program.\n", 198 | "\n", 199 | "Agents can be modeled as [objects](https://python-programming.quantecon.org/python_oop.html).\n", 200 | "\n", 201 | "Here’s an indication of how they might look" 202 | ] 203 | }, 204 | { 205 | "cell_type": "markdown", 206 | "id": "0b06bc85", 207 | "metadata": { 208 | "hide-output": false 209 | }, 210 | "source": [ 211 | "```text\n", 212 | "* Data:\n", 213 | "\n", 214 | " * type (green or orange)\n", 215 | " * location\n", 216 | "\n", 217 | "* Methods:\n", 218 | "\n", 219 | " * determine whether happy or not given locations of other agents\n", 220 | "\n", 221 | " * If not happy, move\n", 222 | "\n", 223 | " * find a new location where happy\n", 224 | "```\n" 225 | ] 226 | }, 227 | { 228 | "cell_type": "markdown", 229 | "id": "e526860e", 230 | "metadata": {}, 231 | "source": [ 232 | "And here’s some pseudocode for the main loop" 233 | ] 234 | }, 235 | { 236 | "cell_type": "markdown", 237 | "id": "cf798afd", 238 | "metadata": { 239 | "hide-output": false 240 | }, 241 | "source": [ 242 | "```text\n", 243 | "while agents are still moving\n", 244 | " for agent in agents\n", 245 | " give agent the opportunity to move\n", 246 | "```\n" 247 | ] 248 | }, 249 | { 250 | "cell_type": "markdown", 251 | "id": "41df4768", 252 | "metadata": {}, 253 | "source": [ 254 | "Use 250 agents of each type." 255 | ] 256 | }, 257 | { 258 | "cell_type": "markdown", 259 | "id": "c9d8a1d1", 260 | "metadata": {}, 261 | "source": [ 262 | "## Solution to[ Exercise 67.1](https://python.quantecon.org/#schelling_ex1)\n", 263 | "\n", 264 | "Here’s one solution that does the job we want.\n", 265 | "\n", 266 | "If you feel like a further exercise, you can probably speed up some of the computations and\n", 267 | "then increase the number of agents." 268 | ] 269 | }, 270 | { 271 | "cell_type": "code", 272 | "execution_count": null, 273 | "id": "1e6fc12a", 274 | "metadata": { 275 | "hide-output": false 276 | }, 277 | "outputs": [], 278 | "source": [ 279 | "seed(10) # For reproducible random numbers\n", 280 | "\n", 281 | "class Agent:\n", 282 | "\n", 283 | " def __init__(self, type):\n", 284 | " self.type = type\n", 285 | " self.draw_location()\n", 286 | "\n", 287 | " def draw_location(self):\n", 288 | " self.location = uniform(0, 1), uniform(0, 1)\n", 289 | "\n", 290 | " def get_distance(self, other):\n", 291 | " \"Computes the euclidean distance between self and other agent.\"\n", 292 | " a = (self.location[0] - other.location[0])**2\n", 293 | " b = (self.location[1] - other.location[1])**2\n", 294 | " return sqrt(a + b)\n", 295 | "\n", 296 | " def happy(self, agents):\n", 297 | " \"True if sufficient number of nearest neighbors are of the same type.\"\n", 298 | " distances = []\n", 299 | " # distances is a list of pairs (d, agent), where d is distance from\n", 300 | " # agent to self\n", 301 | " for agent in agents:\n", 302 | " if self != agent:\n", 303 | " distance = self.get_distance(agent)\n", 304 | " distances.append((distance, agent))\n", 305 | " # == Sort from smallest to largest, according to distance == #\n", 306 | " distances.sort()\n", 307 | " # == Extract the neighboring agents == #\n", 308 | " neighbors = [agent for d, agent in distances[:num_neighbors]]\n", 309 | " # == Count how many neighbors have the same type as self == #\n", 310 | " num_same_type = sum(self.type == agent.type for agent in neighbors)\n", 311 | " return num_same_type >= require_same_type\n", 312 | "\n", 313 | " def update(self, agents):\n", 314 | " \"If not happy, then randomly choose new locations until happy.\"\n", 315 | " while not self.happy(agents):\n", 316 | " self.draw_location()\n", 317 | "\n", 318 | "\n", 319 | "def plot_distribution(agents, cycle_num):\n", 320 | " \"Plot the distribution of agents after cycle_num rounds of the loop.\"\n", 321 | " x_values_0, y_values_0 = [], []\n", 322 | " x_values_1, y_values_1 = [], []\n", 323 | " # == Obtain locations of each type == #\n", 324 | " for agent in agents:\n", 325 | " x, y = agent.location\n", 326 | " if agent.type == 0:\n", 327 | " x_values_0.append(x)\n", 328 | " y_values_0.append(y)\n", 329 | " else:\n", 330 | " x_values_1.append(x)\n", 331 | " y_values_1.append(y)\n", 332 | " fig, ax = plt.subplots(figsize=(8, 8))\n", 333 | " plot_args = {'markersize': 8, 'alpha': 0.6}\n", 334 | " ax.set_facecolor('azure')\n", 335 | " ax.plot(x_values_0, y_values_0, 'o', markerfacecolor='orange', **plot_args)\n", 336 | " ax.plot(x_values_1, y_values_1, 'o', markerfacecolor='green', **plot_args)\n", 337 | " ax.set_title(f'Cycle {cycle_num-1}')\n", 338 | " plt.show()\n", 339 | "\n", 340 | "# == Main == #\n", 341 | "\n", 342 | "num_of_type_0 = 250\n", 343 | "num_of_type_1 = 250\n", 344 | "num_neighbors = 10 # Number of agents regarded as neighbors\n", 345 | "require_same_type = 5 # Want at least this many neighbors to be same type\n", 346 | "\n", 347 | "# == Create a list of agents == #\n", 348 | "agents = [Agent(0) for i in range(num_of_type_0)]\n", 349 | "agents.extend(Agent(1) for i in range(num_of_type_1))\n", 350 | "\n", 351 | "\n", 352 | "count = 1\n", 353 | "# == Loop until none wishes to move == #\n", 354 | "while True:\n", 355 | " print('Entering loop ', count)\n", 356 | " plot_distribution(agents, count)\n", 357 | " count += 1\n", 358 | " no_one_moved = True\n", 359 | " for agent in agents:\n", 360 | " old_location = agent.location\n", 361 | " agent.update(agents)\n", 362 | " if agent.location != old_location:\n", 363 | " no_one_moved = False\n", 364 | " if no_one_moved:\n", 365 | " break\n", 366 | "\n", 367 | "print('Converged, terminating.')" 368 | ] 369 | } 370 | ], 371 | "metadata": { 372 | "date": 1710733976.466355, 373 | "filename": "schelling.md", 374 | "kernelspec": { 375 | "display_name": "Python", 376 | "language": "python3", 377 | "name": "python3" 378 | }, 379 | "title": "Schelling’s Segregation Model" 380 | }, 381 | "nbformat": 4, 382 | "nbformat_minor": 5 383 | } -------------------------------------------------------------------------------- /egm_policy_iter.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "642a6393", 6 | "metadata": {}, 7 | "source": [ 8 | "
\n", 9 | " \n", 10 | " \"QuantEcon\"\n", 11 | " \n", 12 | "
" 13 | ] 14 | }, 15 | { 16 | "cell_type": "markdown", 17 | "id": "0c3facee", 18 | "metadata": {}, 19 | "source": [ 20 | "# Optimal Growth IV: The Endogenous Grid Method" 21 | ] 22 | }, 23 | { 24 | "cell_type": "markdown", 25 | "id": "3641e019", 26 | "metadata": {}, 27 | "source": [ 28 | "## Contents\n", 29 | "\n", 30 | "- [Optimal Growth IV: The Endogenous Grid Method](#Optimal-Growth-IV:-The-Endogenous-Grid-Method) \n", 31 | " - [Overview](#Overview) \n", 32 | " - [Key Idea](#Key-Idea) \n", 33 | " - [Implementation](#Implementation) " 34 | ] 35 | }, 36 | { 37 | "cell_type": "markdown", 38 | "id": "d17745a8", 39 | "metadata": {}, 40 | "source": [ 41 | "## Overview\n", 42 | "\n", 43 | "Previously, we solved the stochastic optimal growth model using\n", 44 | "\n", 45 | "1. [value function iteration](https://python.quantecon.org/optgrowth_fast.html) \n", 46 | "1. [Euler equation based time iteration](https://python.quantecon.org/coleman_policy_iter.html) \n", 47 | "\n", 48 | "\n", 49 | "We found time iteration to be significantly more accurate and efficient.\n", 50 | "\n", 51 | "In this lecture, we’ll look at a clever twist on time iteration called the **endogenous grid method** (EGM).\n", 52 | "\n", 53 | "EGM is a numerical method for implementing policy iteration invented by [Chris Carroll](https://econ.jhu.edu/directory/christopher-carroll/).\n", 54 | "\n", 55 | "The original reference is [[Carroll, 2006](https://python.quantecon.org/zreferences.html#id168)].\n", 56 | "\n", 57 | "Let’s start with some standard imports:" 58 | ] 59 | }, 60 | { 61 | "cell_type": "code", 62 | "execution_count": null, 63 | "id": "7572e2f1", 64 | "metadata": { 65 | "hide-output": false 66 | }, 67 | "outputs": [], 68 | "source": [ 69 | "import matplotlib.pyplot as plt\n", 70 | "import numpy as np\n", 71 | "from numba import jit" 72 | ] 73 | }, 74 | { 75 | "cell_type": "markdown", 76 | "id": "cb928760", 77 | "metadata": {}, 78 | "source": [ 79 | "## Key Idea\n", 80 | "\n", 81 | "Let’s start by reminding ourselves of the theory and then see how the numerics fit in." 82 | ] 83 | }, 84 | { 85 | "cell_type": "markdown", 86 | "id": "204b45cd", 87 | "metadata": {}, 88 | "source": [ 89 | "### Theory\n", 90 | "\n", 91 | "Take the model set out in [the time iteration lecture](https://python.quantecon.org/coleman_policy_iter.html), following the same terminology and notation.\n", 92 | "\n", 93 | "The Euler equation is\n", 94 | "\n", 95 | "\n", 96 | "\n", 97 | "$$\n", 98 | "(u'\\circ \\sigma^*)(y)\n", 99 | "= \\beta \\int (u'\\circ \\sigma^*)(f(y - \\sigma^*(y)) z) f'(y - \\sigma^*(y)) z \\phi(dz) \\tag{61.1}\n", 100 | "$$\n", 101 | "\n", 102 | "As we saw, the Coleman-Reffett operator is a nonlinear operator $ K $ engineered so that $ \\sigma^* $ is a fixed point of $ K $.\n", 103 | "\n", 104 | "It takes as its argument a continuous strictly increasing consumption policy $ \\sigma \\in \\Sigma $.\n", 105 | "\n", 106 | "It returns a new function $ K \\sigma $, where $ (K \\sigma)(y) $ is the $ c \\in (0, \\infty) $ that solves\n", 107 | "\n", 108 | "\n", 109 | "\n", 110 | "$$\n", 111 | "u'(c)\n", 112 | "= \\beta \\int (u' \\circ \\sigma) (f(y - c) z ) f'(y - c) z \\phi(dz) \\tag{61.2}\n", 113 | "$$" 114 | ] 115 | }, 116 | { 117 | "cell_type": "markdown", 118 | "id": "d0ffc4a6", 119 | "metadata": {}, 120 | "source": [ 121 | "### Exogenous Grid\n", 122 | "\n", 123 | "As discussed in [the lecture on time iteration](https://python.quantecon.org/coleman_policy_iter.html), to implement the method on a computer, we need a numerical approximation.\n", 124 | "\n", 125 | "In particular, we represent a policy function by a set of values on a finite grid.\n", 126 | "\n", 127 | "The function itself is reconstructed from this representation when necessary, using interpolation or some other method.\n", 128 | "\n", 129 | "[Previously](https://python.quantecon.org/coleman_policy_iter.html), to obtain a finite representation of an updated consumption policy, we\n", 130 | "\n", 131 | "- fixed a grid of income points $ \\{y_i\\} $ \n", 132 | "- calculated the consumption value $ c_i $ corresponding to each\n", 133 | " $ y_i $ using [(61.2)](#equation-egm-coledef) and a root-finding routine \n", 134 | "\n", 135 | "\n", 136 | "Each $ c_i $ is then interpreted as the value of the function $ K \\sigma $ at $ y_i $.\n", 137 | "\n", 138 | "Thus, with the points $ \\{y_i, c_i\\} $ in hand, we can reconstruct $ K \\sigma $ via approximation.\n", 139 | "\n", 140 | "Iteration then continues…" 141 | ] 142 | }, 143 | { 144 | "cell_type": "markdown", 145 | "id": "4f264951", 146 | "metadata": {}, 147 | "source": [ 148 | "### Endogenous Grid\n", 149 | "\n", 150 | "The method discussed above requires a root-finding routine to find the\n", 151 | "$ c_i $ corresponding to a given income value $ y_i $.\n", 152 | "\n", 153 | "Root-finding is costly because it typically involves a significant number of\n", 154 | "function evaluations.\n", 155 | "\n", 156 | "As pointed out by Carroll [[Carroll, 2006](https://python.quantecon.org/zreferences.html#id168)], we can avoid this if\n", 157 | "$ y_i $ is chosen endogenously.\n", 158 | "\n", 159 | "The only assumption required is that $ u' $ is invertible on $ (0, \\infty) $.\n", 160 | "\n", 161 | "Let $ (u')^{-1} $ be the inverse function of $ u' $.\n", 162 | "\n", 163 | "The idea is this:\n", 164 | "\n", 165 | "- First, we fix an *exogenous* grid $ \\{k_i\\} $ for capital ($ k = y - c $). \n", 166 | "- Then we obtain $ c_i $ via \n", 167 | "\n", 168 | "\n", 169 | "\n", 170 | "\n", 171 | "$$\n", 172 | "c_i =\n", 173 | "(u')^{-1}\n", 174 | "\\left\\{\n", 175 | " \\beta \\int (u' \\circ \\sigma) (f(k_i) z ) \\, f'(k_i) \\, z \\, \\phi(dz)\n", 176 | "\\right\\} \\tag{61.3}\n", 177 | "$$\n", 178 | "\n", 179 | "- Finally, for each $ c_i $ we set $ y_i = c_i + k_i $. \n", 180 | "\n", 181 | "\n", 182 | "It is clear that each $ (y_i, c_i) $ pair constructed in this manner satisfies [(61.2)](#equation-egm-coledef).\n", 183 | "\n", 184 | "With the points $ \\{y_i, c_i\\} $ in hand, we can reconstruct $ K \\sigma $ via approximation as before.\n", 185 | "\n", 186 | "The name EGM comes from the fact that the grid $ \\{y_i\\} $ is determined **endogenously**." 187 | ] 188 | }, 189 | { 190 | "cell_type": "markdown", 191 | "id": "19b1c2bb", 192 | "metadata": {}, 193 | "source": [ 194 | "## Implementation\n", 195 | "\n", 196 | "As [before](https://python.quantecon.org/coleman_policy_iter.html), we will start with a simple setting\n", 197 | "where\n", 198 | "\n", 199 | "- $ u(c) = \\ln c $, \n", 200 | "- production is Cobb-Douglas, and \n", 201 | "- the shocks are lognormal. \n", 202 | "\n", 203 | "\n", 204 | "This will allow us to make comparisons with the analytical solutions" 205 | ] 206 | }, 207 | { 208 | "cell_type": "code", 209 | "execution_count": null, 210 | "id": "c78a7db9", 211 | "metadata": { 212 | "hide-output": false 213 | }, 214 | "outputs": [], 215 | "source": [ 216 | "\n", 217 | "def v_star(y, α, β, μ):\n", 218 | " \"\"\"\n", 219 | " True value function\n", 220 | " \"\"\"\n", 221 | " c1 = np.log(1 - α * β) / (1 - β)\n", 222 | " c2 = (μ + α * np.log(α * β)) / (1 - α)\n", 223 | " c3 = 1 / (1 - β)\n", 224 | " c4 = 1 / (1 - α * β)\n", 225 | " return c1 + c2 * (c3 - c4) + c4 * np.log(y)\n", 226 | "\n", 227 | "def σ_star(y, α, β):\n", 228 | " \"\"\"\n", 229 | " True optimal policy\n", 230 | " \"\"\"\n", 231 | " return (1 - α * β) * y" 232 | ] 233 | }, 234 | { 235 | "cell_type": "markdown", 236 | "id": "caa68a7c", 237 | "metadata": {}, 238 | "source": [ 239 | "We reuse the `OptimalGrowthModel` class" 240 | ] 241 | }, 242 | { 243 | "cell_type": "code", 244 | "execution_count": null, 245 | "id": "71a74289", 246 | "metadata": { 247 | "hide-output": false 248 | }, 249 | "outputs": [], 250 | "source": [ 251 | "from numba import float64\n", 252 | "from numba.experimental import jitclass\n", 253 | "\n", 254 | "opt_growth_data = [\n", 255 | " ('α', float64), # Production parameter\n", 256 | " ('β', float64), # Discount factor\n", 257 | " ('μ', float64), # Shock location parameter\n", 258 | " ('s', float64), # Shock scale parameter\n", 259 | " ('grid', float64[:]), # Grid (array)\n", 260 | " ('shocks', float64[:]) # Shock draws (array)\n", 261 | "]\n", 262 | "\n", 263 | "@jitclass(opt_growth_data)\n", 264 | "class OptimalGrowthModel:\n", 265 | "\n", 266 | " def __init__(self,\n", 267 | " α=0.4,\n", 268 | " β=0.96,\n", 269 | " μ=0,\n", 270 | " s=0.1,\n", 271 | " grid_max=4,\n", 272 | " grid_size=120,\n", 273 | " shock_size=250,\n", 274 | " seed=1234):\n", 275 | "\n", 276 | " self.α, self.β, self.μ, self.s = α, β, μ, s\n", 277 | "\n", 278 | " # Set up grid\n", 279 | " self.grid = np.linspace(1e-5, grid_max, grid_size)\n", 280 | "\n", 281 | " # Store shocks (with a seed, so results are reproducible)\n", 282 | " np.random.seed(seed)\n", 283 | " self.shocks = np.exp(μ + s * np.random.randn(shock_size))\n", 284 | "\n", 285 | "\n", 286 | " def f(self, k):\n", 287 | " \"The production function\"\n", 288 | " return k**self.α\n", 289 | "\n", 290 | "\n", 291 | " def u(self, c):\n", 292 | " \"The utility function\"\n", 293 | " return np.log(c)\n", 294 | "\n", 295 | " def f_prime(self, k):\n", 296 | " \"Derivative of f\"\n", 297 | " return self.α * (k**(self.α - 1))\n", 298 | "\n", 299 | "\n", 300 | " def u_prime(self, c):\n", 301 | " \"Derivative of u\"\n", 302 | " return 1/c\n", 303 | "\n", 304 | " def u_prime_inv(self, c):\n", 305 | " \"Inverse of u'\"\n", 306 | " return 1/c" 307 | ] 308 | }, 309 | { 310 | "cell_type": "markdown", 311 | "id": "ccf7221c", 312 | "metadata": {}, 313 | "source": [ 314 | "### The Operator\n", 315 | "\n", 316 | "Here’s an implementation of $ K $ using EGM as described above." 317 | ] 318 | }, 319 | { 320 | "cell_type": "code", 321 | "execution_count": null, 322 | "id": "d6e7ffd9", 323 | "metadata": { 324 | "hide-output": false 325 | }, 326 | "outputs": [], 327 | "source": [ 328 | "@jit\n", 329 | "def K(σ_array, og):\n", 330 | " \"\"\"\n", 331 | " The Coleman-Reffett operator using EGM\n", 332 | "\n", 333 | " \"\"\"\n", 334 | "\n", 335 | " # Simplify names\n", 336 | " f, β = og.f, og.β\n", 337 | " f_prime, u_prime = og.f_prime, og.u_prime\n", 338 | " u_prime_inv = og.u_prime_inv\n", 339 | " grid, shocks = og.grid, og.shocks\n", 340 | "\n", 341 | " # Determine endogenous grid\n", 342 | " y = grid + σ_array # y_i = k_i + c_i\n", 343 | "\n", 344 | " # Linear interpolation of policy using endogenous grid\n", 345 | " σ = lambda x: np.interp(x, y, σ_array)\n", 346 | "\n", 347 | " # Allocate memory for new consumption array\n", 348 | " c = np.empty_like(grid)\n", 349 | "\n", 350 | " # Solve for updated consumption value\n", 351 | " for i, k in enumerate(grid):\n", 352 | " vals = u_prime(σ(f(k) * shocks)) * f_prime(k) * shocks\n", 353 | " c[i] = u_prime_inv(β * np.mean(vals))\n", 354 | "\n", 355 | " return c" 356 | ] 357 | }, 358 | { 359 | "cell_type": "markdown", 360 | "id": "9ccb0190", 361 | "metadata": {}, 362 | "source": [ 363 | "Note the lack of any root-finding algorithm." 364 | ] 365 | }, 366 | { 367 | "cell_type": "markdown", 368 | "id": "af7a6400", 369 | "metadata": {}, 370 | "source": [ 371 | "### Testing\n", 372 | "\n", 373 | "First we create an instance." 374 | ] 375 | }, 376 | { 377 | "cell_type": "code", 378 | "execution_count": null, 379 | "id": "ac8b5e4d", 380 | "metadata": { 381 | "hide-output": false 382 | }, 383 | "outputs": [], 384 | "source": [ 385 | "og = OptimalGrowthModel()\n", 386 | "grid = og.grid" 387 | ] 388 | }, 389 | { 390 | "cell_type": "markdown", 391 | "id": "8d5adc39", 392 | "metadata": {}, 393 | "source": [ 394 | "Here’s our solver routine:" 395 | ] 396 | }, 397 | { 398 | "cell_type": "code", 399 | "execution_count": null, 400 | "id": "1749f58a", 401 | "metadata": { 402 | "hide-output": false 403 | }, 404 | "outputs": [], 405 | "source": [ 406 | "def solve_model_time_iter(model, # Class with model information\n", 407 | " σ, # Initial condition\n", 408 | " tol=1e-4,\n", 409 | " max_iter=1000,\n", 410 | " verbose=True,\n", 411 | " print_skip=25):\n", 412 | "\n", 413 | " # Set up loop\n", 414 | " i = 0\n", 415 | " error = tol + 1\n", 416 | "\n", 417 | " while i < max_iter and error > tol:\n", 418 | " σ_new = K(σ, model)\n", 419 | " error = np.max(np.abs(σ - σ_new))\n", 420 | " i += 1\n", 421 | " if verbose and i % print_skip == 0:\n", 422 | " print(f\"Error at iteration {i} is {error}.\")\n", 423 | " σ = σ_new\n", 424 | "\n", 425 | " if error > tol:\n", 426 | " print(\"Failed to converge!\")\n", 427 | " elif verbose:\n", 428 | " print(f\"\\nConverged in {i} iterations.\")\n", 429 | "\n", 430 | " return σ_new" 431 | ] 432 | }, 433 | { 434 | "cell_type": "markdown", 435 | "id": "8bf3c238", 436 | "metadata": {}, 437 | "source": [ 438 | "Let’s call it:" 439 | ] 440 | }, 441 | { 442 | "cell_type": "code", 443 | "execution_count": null, 444 | "id": "8393528f", 445 | "metadata": { 446 | "hide-output": false 447 | }, 448 | "outputs": [], 449 | "source": [ 450 | "σ_init = np.copy(grid)\n", 451 | "σ = solve_model_time_iter(og, σ_init)" 452 | ] 453 | }, 454 | { 455 | "cell_type": "markdown", 456 | "id": "fdfc5ff4", 457 | "metadata": {}, 458 | "source": [ 459 | "Here is a plot of the resulting policy, compared with the true policy:" 460 | ] 461 | }, 462 | { 463 | "cell_type": "code", 464 | "execution_count": null, 465 | "id": "b65bfdb1", 466 | "metadata": { 467 | "hide-output": false 468 | }, 469 | "outputs": [], 470 | "source": [ 471 | "y = grid + σ # y_i = k_i + c_i\n", 472 | "\n", 473 | "fig, ax = plt.subplots()\n", 474 | "\n", 475 | "ax.plot(y, σ, lw=2,\n", 476 | " alpha=0.8, label='approximate policy function')\n", 477 | "\n", 478 | "ax.plot(y, σ_star(y, og.α, og.β), 'k--',\n", 479 | " lw=2, alpha=0.8, label='true policy function')\n", 480 | "\n", 481 | "ax.legend()\n", 482 | "plt.show()" 483 | ] 484 | }, 485 | { 486 | "cell_type": "markdown", 487 | "id": "3e13fe87", 488 | "metadata": {}, 489 | "source": [ 490 | "The maximal absolute deviation between the two policies is" 491 | ] 492 | }, 493 | { 494 | "cell_type": "code", 495 | "execution_count": null, 496 | "id": "50c18390", 497 | "metadata": { 498 | "hide-output": false 499 | }, 500 | "outputs": [], 501 | "source": [ 502 | "np.max(np.abs(σ - σ_star(y, og.α, og.β)))" 503 | ] 504 | }, 505 | { 506 | "cell_type": "markdown", 507 | "id": "adce2673", 508 | "metadata": {}, 509 | "source": [ 510 | "How long does it take to converge?" 511 | ] 512 | }, 513 | { 514 | "cell_type": "code", 515 | "execution_count": null, 516 | "id": "49b2ab11", 517 | "metadata": { 518 | "hide-output": false 519 | }, 520 | "outputs": [], 521 | "source": [ 522 | "%%timeit -n 3 -r 1\n", 523 | "σ = solve_model_time_iter(og, σ_init, verbose=False)" 524 | ] 525 | }, 526 | { 527 | "cell_type": "markdown", 528 | "id": "8f210ca1", 529 | "metadata": {}, 530 | "source": [ 531 | "Relative to time iteration, which as already found to be highly efficient, EGM\n", 532 | "has managed to shave off still more run time without compromising accuracy.\n", 533 | "\n", 534 | "This is due to the lack of a numerical root-finding step.\n", 535 | "\n", 536 | "We can now solve the optimal growth model at given parameters extremely fast." 537 | ] 538 | } 539 | ], 540 | "metadata": { 541 | "date": 1762377773.044283, 542 | "filename": "egm_policy_iter.md", 543 | "kernelspec": { 544 | "display_name": "Python", 545 | "language": "python3", 546 | "name": "python3" 547 | }, 548 | "title": "Optimal Growth IV: The Endogenous Grid Method" 549 | }, 550 | "nbformat": 4, 551 | "nbformat_minor": 5 552 | } -------------------------------------------------------------------------------- /rand_resp.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "fac619a9", 6 | "metadata": {}, 7 | "source": [ 8 | "# Randomized Response Surveys" 9 | ] 10 | }, 11 | { 12 | "cell_type": "markdown", 13 | "id": "678daa6d", 14 | "metadata": {}, 15 | "source": [ 16 | "## Overview\n", 17 | "\n", 18 | "Social stigmas can inhibit people from confessing potentially embarrassing activities or opinions.\n", 19 | "\n", 20 | "When people are reluctant to participate a sample survey about personally sensitive issues, they might decline to participate, and even if they do participate, they might choose to provide incorrect answers to sensitive questions.\n", 21 | "\n", 22 | "These problems induce **selection** biases that present challenges to interpreting and designing surveys.\n", 23 | "\n", 24 | "To illustrate how social scientists have thought about estimating the prevalence of such embarrassing activities and opinions, this lecture describes a classic approach of S. L. Warner [[Warner, 1965](https://python.quantecon.org/zreferences.html#id23)].\n", 25 | "\n", 26 | "Warner used elementary probability to construct a way to protect the privacy of **individual** respondents to surveys while still estimating the fraction of a **collection** of individuals who have a socially stigmatized characteristic or who engage in a socially stigmatized activity.\n", 27 | "\n", 28 | "Warner’s idea was to add **noise** between the respondent’s answer and the **signal** about that answer that the survey maker ultimately receives.\n", 29 | "\n", 30 | "Knowing about the structure of the noise assures the respondent that the survey maker does not observe his answer.\n", 31 | "\n", 32 | "Statistical properties of the noise injection procedure provide the respondent **plausible deniability**.\n", 33 | "\n", 34 | "Related ideas underlie modern **differential privacy** systems.\n", 35 | "\n", 36 | "(See [https://en.wikipedia.org/wiki/Differential_privacy](https://en.wikipedia.org/wiki/Differential_privacy))" 37 | ] 38 | }, 39 | { 40 | "cell_type": "markdown", 41 | "id": "e4bf65aa", 42 | "metadata": {}, 43 | "source": [ 44 | "## Warner’s Strategy\n", 45 | "\n", 46 | "As usual, let’s bring in the Python modules we’ll be using." 47 | ] 48 | }, 49 | { 50 | "cell_type": "code", 51 | "execution_count": null, 52 | "id": "bde1ce1c", 53 | "metadata": { 54 | "hide-output": false 55 | }, 56 | "outputs": [], 57 | "source": [ 58 | "import numpy as np\n", 59 | "import pandas as pd" 60 | ] 61 | }, 62 | { 63 | "cell_type": "markdown", 64 | "id": "1297d5e4", 65 | "metadata": {}, 66 | "source": [ 67 | "Suppose that every person in population either belongs to Group A or Group B.\n", 68 | "\n", 69 | "We want to estimate the proportion $ \\pi $ who belong to Group A while protecting individual respondents’ privacy.\n", 70 | "\n", 71 | "Warner [[Warner, 1965](https://python.quantecon.org/zreferences.html#id23)] proposed and analyzed the following procedure.\n", 72 | "\n", 73 | "- A random sample of $ n $ people is drawn with replacement from the population and each person is interviewed. \n", 74 | "- Draw $ n $ random samples from the population with replacement and interview each person. \n", 75 | "- Prepare a **random spinner** that with $ p $ probability points to the Letter A and with $ (1-p) $ probability points to the Letter B. \n", 76 | "- Each subject spins a random spinner and sees an outcome (A or B) that the interviewer does **not observe**. \n", 77 | "- The subject states whether he belongs to the group to which the spinner points. \n", 78 | "- If the spinner points to the group that the spinner belongs, the subject reports “yes”; otherwise he reports “no”. \n", 79 | "- The subject answers the question truthfully. \n", 80 | "\n", 81 | "\n", 82 | "Warner constructed a maximum likelihood estimators of the proportion of the population in set A.\n", 83 | "\n", 84 | "Let\n", 85 | "\n", 86 | "- $ \\pi $ : True probability of A in the population \n", 87 | "- $ p $ : Probability that the spinner points to A \n", 88 | "- $ X_{i}=\\begin{cases}1,\\text{ if the } i\\text{th} \\ \\text{ subject says yes}\\\\0,\\text{ if the } i\\text{th} \\ \\text{ subject says no}\\end{cases} $ \n", 89 | "\n", 90 | "\n", 91 | "Index the sample set so that the first $ n_1 $ report “yes”, while the second $ n-n_1 $ report “no”.\n", 92 | "\n", 93 | "The likelihood function of a sample set is\n", 94 | "\n", 95 | "\n", 96 | "\n", 97 | "$$\n", 98 | "L=\\left[\\pi p + (1-\\pi)(1-p)\\right]^{n_{1}}\\left[(1-\\pi) p +\\pi (1-p)\\right]^{n-n_{1}} \\tag{16.1}\n", 99 | "$$\n", 100 | "\n", 101 | "The log of the likelihood function is:\n", 102 | "\n", 103 | "\n", 104 | "\n", 105 | "$$\n", 106 | "\\log(L)= n_1 \\log \\left[\\pi p + (1-\\pi)(1-p)\\right] + (n-n_{1}) \\log \\left[(1-\\pi) p +\\pi (1-p)\\right] \\tag{16.2}\n", 107 | "$$\n", 108 | "\n", 109 | "The first-order necessary condition for maximizing the log likelihood function with respect to $ \\pi $ is:\n", 110 | "\n", 111 | "$$\n", 112 | "\\frac{(n-n_1)(2p-1)}{(1-\\pi) p +\\pi (1-p)}=\\frac{n_1 (2p-1)}{\\pi p + (1-\\pi)(1-p)}\n", 113 | "$$\n", 114 | "\n", 115 | "or\n", 116 | "\n", 117 | "\n", 118 | "\n", 119 | "$$\n", 120 | "\\pi p + (1-\\pi)(1-p)=\\frac{n_1}{n} \\tag{16.3}\n", 121 | "$$\n", 122 | "\n", 123 | "If $ p \\neq \\frac{1}{2} $, then the maximum likelihood estimator (MLE) of $ \\pi $ is:\n", 124 | "\n", 125 | "\n", 126 | "\n", 127 | "$$\n", 128 | "\\hat{\\pi}=\\frac{p-1}{2p-1}+\\frac{n_1}{(2p-1)n} \\tag{16.4}\n", 129 | "$$\n", 130 | "\n", 131 | "We compute the mean and variance of the MLE estimator $ \\hat \\pi $ to be:\n", 132 | "\n", 133 | "\n", 134 | "\n", 135 | "$$\n", 136 | "\\begin{aligned}\n", 137 | "\\mathbb{E}(\\hat{\\pi})&= \\frac{1}{2 p-1}\\left[p-1+\\frac{1}{n} \\sum_{i=1}^{n} \\mathbb{E} X_i \\right] \\\\\n", 138 | "&=\\frac{1}{2 p-1} \\left[ p -1 + \\pi p + (1-\\pi)(1-p)\\right] \\\\\n", 139 | "&=\\pi\n", 140 | "\\end{aligned} \\tag{16.5}\n", 141 | "$$\n", 142 | "\n", 143 | "and\n", 144 | "\n", 145 | "\n", 146 | "\n", 147 | "$$\n", 148 | "\\begin{aligned}\n", 149 | "Var(\\hat{\\pi})&=\\frac{n Var(X_i)}{(2p - 1 )^2 n^2} \\\\\n", 150 | "&= \\frac{\\left[\\pi p + (1-\\pi)(1-p)\\right]\\left[(1-\\pi) p +\\pi (1-p)\\right]}{(2p - 1 )^2 n^2}\\\\\n", 151 | "&=\\frac{\\frac{1}{4}+(2 p^2 - 2 p +\\frac{1}{2})(- 2 \\pi^2 + 2 \\pi -\\frac{1}{2})}{(2p - 1 )^2 n^2}\\\\\n", 152 | "&=\\frac{1}{n}\\left[\\frac{1}{16(p-\\frac{1}{2})^2}-(\\pi-\\frac{1}{2})^2 \\right]\n", 153 | "\\end{aligned} \\tag{16.6}\n", 154 | "$$\n", 155 | "\n", 156 | "Equation [(16.5)](#equation-eq-five) indicates that $ \\hat{\\pi} $ is an **unbiased estimator** of $ \\pi $ while equation [(16.6)](#equation-eq-six) tell us the variance of the estimator.\n", 157 | "\n", 158 | "To compute a confidence interval, first rewrite [(16.6)](#equation-eq-six) as:\n", 159 | "\n", 160 | "\n", 161 | "\n", 162 | "$$\n", 163 | "Var(\\hat{\\pi})=\\frac{\\frac{1}{4}-(\\pi-\\frac{1}{2})^2}{n}+\\frac{\\frac{1}{16(p-\\frac{1}{2})^2}-\\frac{1}{4}}{n} \\tag{16.7}\n", 164 | "$$\n", 165 | "\n", 166 | "This equation indicates that the variance of $ \\hat{\\pi} $ can be represented as a sum of the variance due to sampling plus the variance due to the random device.\n", 167 | "\n", 168 | "From the expressions above we can find that:\n", 169 | "\n", 170 | "- When $ p $ is $ \\frac{1}{2} $, expression [(16.1)](#equation-eq-one) degenerates to a constant. \n", 171 | "- When $ p $ is $ 1 $ or $ 0 $, the randomized estimate degenerates to an estimator without randomized sampling. \n", 172 | "\n", 173 | "\n", 174 | "We shall only discuss situations in which $ p \\in (\\frac{1}{2},1) $\n", 175 | "\n", 176 | "(a situation in which $ p \\in (0,\\frac{1}{2}) $ is symmetric).\n", 177 | "\n", 178 | "From expressions [(16.5)](#equation-eq-five) and [(16.7)](#equation-eq-seven) we can deduce that:\n", 179 | "\n", 180 | "- The MSE of $ \\hat{\\pi} $ decreases as $ p $ increases. " 181 | ] 182 | }, 183 | { 184 | "cell_type": "markdown", 185 | "id": "a8aad786", 186 | "metadata": {}, 187 | "source": [ 188 | "## Comparing Two Survey Designs\n", 189 | "\n", 190 | "Let’s compare the preceding randomized-response method with a stylized non-randomized response method.\n", 191 | "\n", 192 | "In our non-randomized response method, we suppose that:\n", 193 | "\n", 194 | "- Members of Group A tells the truth with probability $ T_a $ while the members of Group B tells the truth with probability $ T_b $ \n", 195 | "- $ Y_i $ is $ 1 $ or $ 0 $ according to whether the sample’s $ i\\text{th} $ member’s report is in Group A or not. \n", 196 | "\n", 197 | "\n", 198 | "Then we can estimate $ \\pi $ as:\n", 199 | "\n", 200 | "\n", 201 | "\n", 202 | "$$\n", 203 | "\\hat{\\pi}=\\frac{\\sum_{i=1}^{n}Y_i}{n} \\tag{16.8}\n", 204 | "$$\n", 205 | "\n", 206 | "We calculate the expectation, bias, and variance of the estimator to be:\n", 207 | "\n", 208 | "\n", 209 | "\n", 210 | "$$\n", 211 | "\\begin{aligned}\n", 212 | "\\mathbb{E}(\\hat{\\pi})&=\\pi T_a + \\left[ (1-\\pi)(1-T_b)\\right]\\\\\n", 213 | "\\end{aligned} \\tag{16.9}\n", 214 | "$$\n", 215 | "\n", 216 | "\n", 217 | "\n", 218 | "$$\n", 219 | "\\begin{aligned}\n", 220 | "Bias(\\hat{\\pi})&=\\mathbb{E}(\\hat{\\pi}-\\pi)\\\\\n", 221 | "&=\\pi [T_a + T_b -2 ] + [1- T_b] \\\\\n", 222 | "\\end{aligned} \\tag{16.10}\n", 223 | "$$\n", 224 | "\n", 225 | "\n", 226 | "\n", 227 | "$$\n", 228 | "\\begin{aligned}\n", 229 | "Var(\\hat{\\pi})&=\\frac{ \\left[ \\pi T_a + (1-\\pi)(1-T_b)\\right] \\left[1- \\pi T_a -(1-\\pi)(1-T_b)\\right] }{n}\n", 230 | "\\end{aligned} \\tag{16.11}\n", 231 | "$$\n", 232 | "\n", 233 | "It is useful to define a\n", 234 | "\n", 235 | "$$\n", 236 | "\\text{MSE Ratio}=\\frac{\\text{Mean Square Error Randomized}}{\\text{Mean Square Error Regular}}\n", 237 | "$$\n", 238 | "\n", 239 | "We can compute MSE Ratios for different survey designs associated with different parameter values.\n", 240 | "\n", 241 | "The following Python code computes objects we want to stare at in order to make comparisons\n", 242 | "under different values of $ \\pi_A $ and $ n $:" 243 | ] 244 | }, 245 | { 246 | "cell_type": "code", 247 | "execution_count": null, 248 | "id": "2e443e87", 249 | "metadata": { 250 | "hide-output": false 251 | }, 252 | "outputs": [], 253 | "source": [ 254 | "class Comparison:\n", 255 | " def __init__(self, A, n):\n", 256 | " self.A = A\n", 257 | " self.n = n\n", 258 | " TaTb = np.array([[0.95, 1], [0.9, 1], [0.7, 1], \n", 259 | " [0.5, 1], [1, 0.95], [1, 0.9], \n", 260 | " [1, 0.7], [1, 0.5], [0.95, 0.95], \n", 261 | " [0.9, 0.9], [0.7, 0.7], [0.5, 0.5]])\n", 262 | " self.p_arr = np.array([0.6, 0.7, 0.8, 0.9])\n", 263 | " self.p_map = dict(zip(self.p_arr, [f\"MSE Ratio: p = {x}\" for x in self.p_arr]))\n", 264 | " self.template = pd.DataFrame(columns=self.p_arr)\n", 265 | " self.template[['T_a','T_b']] = TaTb\n", 266 | " self.template['Bias'] = None\n", 267 | " \n", 268 | " def theoretical(self):\n", 269 | " A = self.A\n", 270 | " n = self.n\n", 271 | " df = self.template.copy()\n", 272 | " df['Bias'] = A * (df['T_a'] + df['T_b'] - 2) + (1 - df['T_b'])\n", 273 | " for p in self.p_arr:\n", 274 | " df[p] = (1 / (16 * (p - 1/2)**2) - (A - 1/2)**2) / n / \\\n", 275 | " (df['Bias']**2 + ((A * df['T_a'] + (1 - A) * (1 - df['T_b'])) * (1 - A * df['T_a'] - (1 - A) * (1 - df['T_b'])) / n))\n", 276 | " df[p] = df[p].round(2)\n", 277 | " df = df.set_index([\"T_a\", \"T_b\", \"Bias\"]).rename(columns=self.p_map)\n", 278 | " return df\n", 279 | " \n", 280 | " def MCsimulation(self, size=1000, seed=123456):\n", 281 | " A = self.A\n", 282 | " n = self.n\n", 283 | " df = self.template.copy()\n", 284 | " np.random.seed(seed)\n", 285 | " sample = np.random.rand(size, self.n) <= A\n", 286 | " random_device = np.random.rand(size, n)\n", 287 | " mse_rd = {}\n", 288 | " for p in self.p_arr:\n", 289 | " spinner = random_device <= p\n", 290 | " rd_answer = sample * spinner + (1 - sample) * (1 - spinner)\n", 291 | " n1 = rd_answer.sum(axis=1)\n", 292 | " pi_hat = (p - 1) / (2 * p - 1) + n1 / n / (2 * p - 1)\n", 293 | " mse_rd[p] = np.sum((pi_hat - A)**2)\n", 294 | " for inum, irow in df.iterrows():\n", 295 | " truth_a = np.random.rand(size, self.n) <= irow.T_a\n", 296 | " truth_b = np.random.rand(size, self.n) <= irow.T_b\n", 297 | " trad_answer = sample * truth_a + (1 - sample) * (1 - truth_b)\n", 298 | " pi_trad = trad_answer.sum(axis=1) / n\n", 299 | " df.loc[inum, 'Bias'] = pi_trad.mean() - A\n", 300 | " mse_trad = np.sum((pi_trad - A)**2)\n", 301 | " for p in self.p_arr:\n", 302 | " df.loc[inum, p] = (mse_rd[p] / mse_trad).round(2)\n", 303 | " df = df.set_index([\"T_a\", \"T_b\", \"Bias\"]).rename(columns=self.p_map)\n", 304 | " return df" 305 | ] 306 | }, 307 | { 308 | "cell_type": "markdown", 309 | "id": "d20ebbf7", 310 | "metadata": {}, 311 | "source": [ 312 | "Let’s put the code to work for parameter values\n", 313 | "\n", 314 | "- $ \\pi_A=0.6 $ \n", 315 | "- $ n=1000 $ \n", 316 | "\n", 317 | "\n", 318 | "We can generate MSE Ratios theoretically using the above formulas.\n", 319 | "\n", 320 | "We can also perform Monte Carlo simulations of a MSE Ratio." 321 | ] 322 | }, 323 | { 324 | "cell_type": "code", 325 | "execution_count": null, 326 | "id": "43919ceb", 327 | "metadata": { 328 | "hide-output": false 329 | }, 330 | "outputs": [], 331 | "source": [ 332 | "cp1 = Comparison(0.6, 1000)\n", 333 | "df1_theoretical = cp1.theoretical()\n", 334 | "df1_theoretical" 335 | ] 336 | }, 337 | { 338 | "cell_type": "code", 339 | "execution_count": null, 340 | "id": "6e667ab7", 341 | "metadata": { 342 | "hide-output": false 343 | }, 344 | "outputs": [], 345 | "source": [ 346 | "df1_mc = cp1.MCsimulation()\n", 347 | "df1_mc" 348 | ] 349 | }, 350 | { 351 | "cell_type": "markdown", 352 | "id": "21f90a01", 353 | "metadata": {}, 354 | "source": [ 355 | "The theoretical calculations do a good job of predicting Monte Carlo results.\n", 356 | "\n", 357 | "We see that in many situations, especially when the bias is not small, the MSE of the randomized-sampling methods is smaller than that of the non-randomized sampling method.\n", 358 | "\n", 359 | "These differences become larger as $ p $ increases.\n", 360 | "\n", 361 | "By adjusting parameters $ \\pi_A $ and $ n $, we can study outcomes in different situations.\n", 362 | "\n", 363 | "For example, for another situation described in Warner [[Warner, 1965](https://python.quantecon.org/zreferences.html#id23)]:\n", 364 | "\n", 365 | "- $ \\pi_A=0.5 $ \n", 366 | "- $ n=1000 $ \n", 367 | "\n", 368 | "\n", 369 | "we can use the code" 370 | ] 371 | }, 372 | { 373 | "cell_type": "code", 374 | "execution_count": null, 375 | "id": "d8df2645", 376 | "metadata": { 377 | "hide-output": false 378 | }, 379 | "outputs": [], 380 | "source": [ 381 | "cp2 = Comparison(0.5, 1000)\n", 382 | "df2_theoretical = cp2.theoretical()\n", 383 | "df2_theoretical" 384 | ] 385 | }, 386 | { 387 | "cell_type": "code", 388 | "execution_count": null, 389 | "id": "b584197a", 390 | "metadata": { 391 | "hide-output": false 392 | }, 393 | "outputs": [], 394 | "source": [ 395 | "df2_mc = cp2.MCsimulation()\n", 396 | "df2_mc" 397 | ] 398 | }, 399 | { 400 | "cell_type": "markdown", 401 | "id": "3a287372", 402 | "metadata": {}, 403 | "source": [ 404 | "We can also revisit a calculation in the concluding section of Warner [[Warner, 1965](https://python.quantecon.org/zreferences.html#id23)] in which\n", 405 | "\n", 406 | "- $ \\pi_A=0.6 $ \n", 407 | "- $ n=2000 $ \n", 408 | "\n", 409 | "\n", 410 | "We use the code" 411 | ] 412 | }, 413 | { 414 | "cell_type": "code", 415 | "execution_count": null, 416 | "id": "d463eae5", 417 | "metadata": { 418 | "hide-output": false 419 | }, 420 | "outputs": [], 421 | "source": [ 422 | "cp3 = Comparison(0.6, 2000)\n", 423 | "df3_theoretical = cp3.theoretical()\n", 424 | "df3_theoretical" 425 | ] 426 | }, 427 | { 428 | "cell_type": "code", 429 | "execution_count": null, 430 | "id": "d06079cc", 431 | "metadata": { 432 | "hide-output": false 433 | }, 434 | "outputs": [], 435 | "source": [ 436 | "df3_mc = cp3.MCsimulation()\n", 437 | "df3_mc" 438 | ] 439 | }, 440 | { 441 | "cell_type": "markdown", 442 | "id": "d4ff40b7", 443 | "metadata": {}, 444 | "source": [ 445 | "Evidently, as $ n $ increases, the randomized response method does better performance in more situations." 446 | ] 447 | }, 448 | { 449 | "cell_type": "markdown", 450 | "id": "f1cd5db0", 451 | "metadata": {}, 452 | "source": [ 453 | "## Concluding Remarks\n", 454 | "\n", 455 | "[This QuantEcon lecture](https://python.quantecon.org/util_rand_resp.html) describes some alternative randomized response surveys.\n", 456 | "\n", 457 | "That lecture presents a utilitarian analysis of those alternatives conducted by Lars Ljungqvist\n", 458 | "[[Ljungqvist, 1993](https://python.quantecon.org/zreferences.html#id24)]." 459 | ] 460 | } 461 | ], 462 | "metadata": { 463 | "date": 1764675755.2406225, 464 | "filename": "rand_resp.md", 465 | "kernelspec": { 466 | "display_name": "Python", 467 | "language": "python3", 468 | "name": "python3" 469 | }, 470 | "title": "Randomized Response Surveys" 471 | }, 472 | "nbformat": 4, 473 | "nbformat_minor": 5 474 | } -------------------------------------------------------------------------------- /os_egm.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "41902a14", 6 | "metadata": {}, 7 | "source": [ 8 | "
\n", 9 | " \n", 10 | " \"QuantEcon\"\n", 11 | " \n", 12 | "
" 13 | ] 14 | }, 15 | { 16 | "cell_type": "markdown", 17 | "id": "50125c8a", 18 | "metadata": {}, 19 | "source": [ 20 | "# Optimal Savings V: The Endogenous Grid Method" 21 | ] 22 | }, 23 | { 24 | "cell_type": "markdown", 25 | "id": "87178c86", 26 | "metadata": {}, 27 | "source": [ 28 | "## Contents\n", 29 | "\n", 30 | "- [Optimal Savings V: The Endogenous Grid Method](#Optimal-Savings-V:-The-Endogenous-Grid-Method) \n", 31 | " - [Overview](#Overview) \n", 32 | " - [Key Idea](#Key-Idea) \n", 33 | " - [Implementation](#Implementation) " 34 | ] 35 | }, 36 | { 37 | "cell_type": "markdown", 38 | "id": "60437ce1", 39 | "metadata": {}, 40 | "source": [ 41 | "## Overview\n", 42 | "\n", 43 | "Previously, we solved the optimal savings problem using\n", 44 | "\n", 45 | "1. [value function iteration](https://python.quantecon.org/os_stochastic.html) \n", 46 | "1. [Euler equation based time iteration](https://python.quantecon.org/os_time_iter.html) \n", 47 | "\n", 48 | "\n", 49 | "We found time iteration to be significantly more accurate and efficient.\n", 50 | "\n", 51 | "In this lecture, we’ll look at a clever twist on time iteration called the **endogenous grid method** (EGM).\n", 52 | "\n", 53 | "EGM is a numerical method for implementing policy iteration invented by [Chris Carroll](https://econ.jhu.edu/directory/christopher-carroll/).\n", 54 | "\n", 55 | "The original reference is [[Carroll, 2006](https://python.quantecon.org/zreferences.html#id168)].\n", 56 | "\n", 57 | "For now we will focus on a clean and simple implementation of EGM that stays\n", 58 | "close to the underlying mathematics.\n", 59 | "\n", 60 | "Then, in [Optimal Savings VI: EGM with JAX](https://python.quantecon.org/os_egm_jax.html), we will construct a fully vectorized and parallelized version of EGM based on JAX.\n", 61 | "\n", 62 | "Let’s start with some standard imports:" 63 | ] 64 | }, 65 | { 66 | "cell_type": "code", 67 | "execution_count": null, 68 | "id": "6caf91d9", 69 | "metadata": { 70 | "hide-output": false 71 | }, 72 | "outputs": [], 73 | "source": [ 74 | "import matplotlib.pyplot as plt\n", 75 | "import numpy as np\n", 76 | "import quantecon as qe" 77 | ] 78 | }, 79 | { 80 | "cell_type": "markdown", 81 | "id": "9b24e1ce", 82 | "metadata": {}, 83 | "source": [ 84 | "## Key Idea\n", 85 | "\n", 86 | "First we remind ourselves of the theory and then we turn to numerical methods." 87 | ] 88 | }, 89 | { 90 | "cell_type": "markdown", 91 | "id": "59263de4", 92 | "metadata": {}, 93 | "source": [ 94 | "### Theory\n", 95 | "\n", 96 | "We work with the model set out in [Optimal Savings IV: Time Iteration](https://python.quantecon.org/os_time_iter.html), following the same terminology and notation.\n", 97 | "\n", 98 | "As we saw, the Coleman-Reffett operator is a nonlinear operator $ K $ engineered so that the optimal policy\n", 99 | "$ \\sigma^* $ is a fixed point of $ K $.\n", 100 | "\n", 101 | "It takes as its argument a continuous strictly increasing consumption policy $ \\sigma \\in \\Sigma $.\n", 102 | "\n", 103 | "It returns a new function $ K \\sigma $, where $ (K \\sigma)(x) $ is the $ c \\in (0, \\infty) $ that solves\n", 104 | "\n", 105 | "\n", 106 | "\n", 107 | "$$\n", 108 | "u'(c)\n", 109 | "= \\beta \\int (u' \\circ \\sigma) (f(x - c) z ) f'(x - c) z \\phi(dz) \\tag{55.1}\n", 110 | "$$" 111 | ] 112 | }, 113 | { 114 | "cell_type": "markdown", 115 | "id": "f30f4a25", 116 | "metadata": {}, 117 | "source": [ 118 | "### Exogenous Grid\n", 119 | "\n", 120 | "As discussed in [Optimal Savings IV: Time Iteration](https://python.quantecon.org/os_time_iter.html), to implement the method on a\n", 121 | "computer, we represent a policy function by a set of values on a finite grid.\n", 122 | "\n", 123 | "The function itself is reconstructed from this representation when necessary,\n", 124 | "using interpolation or some other method.\n", 125 | "\n", 126 | "Our previous strategy in [Optimal Savings IV: Time Iteration](https://python.quantecon.org/os_time_iter.html) for obtaining a finite representation of an updated consumption policy was to\n", 127 | "\n", 128 | "- fix a grid of income points $ \\{x_i\\} $ \n", 129 | "- calculate the consumption value $ c_i $ corresponding to each $ x_i $ using\n", 130 | " [(55.1)](#equation-egm-coledef) and a root-finding routine \n", 131 | "\n", 132 | "\n", 133 | "Each $ c_i $ is then interpreted as the value of the function $ K \\sigma $ at $ x_i $.\n", 134 | "\n", 135 | "Thus, with the pairs $ \\{(x_i, c_i)\\} $ in hand, we can reconstruct $ K \\sigma $ via approximation.\n", 136 | "\n", 137 | "Iteration then continues…" 138 | ] 139 | }, 140 | { 141 | "cell_type": "markdown", 142 | "id": "38760d3b", 143 | "metadata": {}, 144 | "source": [ 145 | "### Endogenous Grid\n", 146 | "\n", 147 | "The method discussed above requires a root-finding routine to find the\n", 148 | "$ c_i $ corresponding to a given income value $ x_i $.\n", 149 | "\n", 150 | "Root-finding is costly because it typically involves a significant number of\n", 151 | "function evaluations.\n", 152 | "\n", 153 | "As pointed out by Carroll [[Carroll, 2006](https://python.quantecon.org/zreferences.html#id168)], we can avoid this step if\n", 154 | "$ x_i $ is chosen endogenously.\n", 155 | "\n", 156 | "The only assumption required is that $ u' $ is invertible on $ (0, \\infty) $.\n", 157 | "\n", 158 | "Let $ (u')^{-1} $ be the inverse function of $ u' $.\n", 159 | "\n", 160 | "The idea is this:\n", 161 | "\n", 162 | "- First, we fix an *exogenous* grid $ \\{s_i\\} $ for savings ($ s = x - c $). \n", 163 | "- Then we obtain $ c_i $ via \n", 164 | "\n", 165 | "\n", 166 | "\n", 167 | "\n", 168 | "$$\n", 169 | "c_i =\n", 170 | "(u')^{-1}\n", 171 | "\\left\\{\n", 172 | " \\beta \\int (u' \\circ \\sigma) (f(s_i) z ) \\, f'(s_i) \\, z \\, \\phi(dz)\n", 173 | "\\right\\} \\tag{55.2}\n", 174 | "$$\n", 175 | "\n", 176 | "- Finally, for each $ c_i $ we set $ x_i = c_i + s_i $. \n", 177 | "\n", 178 | "\n", 179 | "Importantly, each $ (x_i, c_i) $ pair constructed in this manner satisfies [(55.1)](#equation-egm-coledef).\n", 180 | "\n", 181 | "With the points $ \\{x_i, c_i\\} $ in hand, we can reconstruct $ K \\sigma $ via approximation as before.\n", 182 | "\n", 183 | "The name EGM comes from the fact that the grid $ \\{x_i\\} $ is determined **endogenously**." 184 | ] 185 | }, 186 | { 187 | "cell_type": "markdown", 188 | "id": "82cee3fb", 189 | "metadata": {}, 190 | "source": [ 191 | "## Implementation\n", 192 | "\n", 193 | "As in [Optimal Savings IV: Time Iteration](https://python.quantecon.org/os_time_iter.html), we will start with a simple setting where\n", 194 | "\n", 195 | "- $ u(c) = \\ln c $, \n", 196 | "- the function $ f $ has a Cobb-Douglas specification, and \n", 197 | "- the shocks are lognormal. \n", 198 | "\n", 199 | "\n", 200 | "This will allow us to make comparisons with the analytical solutions." 201 | ] 202 | }, 203 | { 204 | "cell_type": "code", 205 | "execution_count": null, 206 | "id": "15b09ea0", 207 | "metadata": { 208 | "hide-output": false 209 | }, 210 | "outputs": [], 211 | "source": [ 212 | "def v_star(x, α, β, μ):\n", 213 | " \"\"\"\n", 214 | " True value function\n", 215 | " \"\"\"\n", 216 | " c1 = np.log(1 - α * β) / (1 - β)\n", 217 | " c2 = (μ + α * np.log(α * β)) / (1 - α)\n", 218 | " c3 = 1 / (1 - β)\n", 219 | " c4 = 1 / (1 - α * β)\n", 220 | " return c1 + c2 * (c3 - c4) + c4 * np.log(x)\n", 221 | "\n", 222 | "def σ_star(x, α, β):\n", 223 | " \"\"\"\n", 224 | " True optimal policy\n", 225 | " \"\"\"\n", 226 | " return (1 - α * β) * x" 227 | ] 228 | }, 229 | { 230 | "cell_type": "markdown", 231 | "id": "c9e9de05", 232 | "metadata": {}, 233 | "source": [ 234 | "We reuse the `Model` structure from [Optimal Savings IV: Time Iteration](https://python.quantecon.org/os_time_iter.html)." 235 | ] 236 | }, 237 | { 238 | "cell_type": "code", 239 | "execution_count": null, 240 | "id": "0638ae53", 241 | "metadata": { 242 | "hide-output": false 243 | }, 244 | "outputs": [], 245 | "source": [ 246 | "from typing import NamedTuple, Callable\n", 247 | "\n", 248 | "class Model(NamedTuple):\n", 249 | " u: Callable # utility function\n", 250 | " f: Callable # production function\n", 251 | " β: float # discount factor\n", 252 | " μ: float # shock location parameter\n", 253 | " ν: float # shock scale parameter\n", 254 | " s_grid: np.ndarray # exogenous savings grid\n", 255 | " shocks: np.ndarray # shock draws\n", 256 | " α: float # production function parameter\n", 257 | " u_prime: Callable # derivative of utility\n", 258 | " f_prime: Callable # derivative of production\n", 259 | " u_prime_inv: Callable # inverse of u_prime\n", 260 | "\n", 261 | "\n", 262 | "def create_model(\n", 263 | " u: Callable,\n", 264 | " f: Callable,\n", 265 | " β: float = 0.96,\n", 266 | " μ: float = 0.0,\n", 267 | " ν: float = 0.1,\n", 268 | " grid_max: float = 4.0,\n", 269 | " grid_size: int = 120,\n", 270 | " shock_size: int = 250,\n", 271 | " seed: int = 1234,\n", 272 | " α: float = 0.4,\n", 273 | " u_prime: Callable = None,\n", 274 | " f_prime: Callable = None,\n", 275 | " u_prime_inv: Callable = None\n", 276 | " ) -> Model:\n", 277 | " \"\"\"\n", 278 | " Creates an instance of the optimal savings model.\n", 279 | " \"\"\"\n", 280 | " # Set up exogenous savings grid\n", 281 | " s_grid = np.linspace(1e-4, grid_max, grid_size)\n", 282 | "\n", 283 | " # Store shocks (with a seed, so results are reproducible)\n", 284 | " np.random.seed(seed)\n", 285 | " shocks = np.exp(μ + ν * np.random.randn(shock_size))\n", 286 | "\n", 287 | " return Model(\n", 288 | " u, f, β, μ, ν, s_grid, shocks, α, u_prime, f_prime, u_prime_inv\n", 289 | " )" 290 | ] 291 | }, 292 | { 293 | "cell_type": "markdown", 294 | "id": "35fea9e0", 295 | "metadata": {}, 296 | "source": [ 297 | "### The Operator\n", 298 | "\n", 299 | "Here’s an implementation of $ K $ using EGM as described above." 300 | ] 301 | }, 302 | { 303 | "cell_type": "code", 304 | "execution_count": null, 305 | "id": "c7ebc38f", 306 | "metadata": { 307 | "hide-output": false 308 | }, 309 | "outputs": [], 310 | "source": [ 311 | "def K(\n", 312 | " c_in: np.ndarray, # Consumption values on the endogenous grid\n", 313 | " x_in: np.ndarray, # Current endogenous grid\n", 314 | " model: Model # Model specification\n", 315 | " ):\n", 316 | " \"\"\"\n", 317 | " An implementation of the Coleman-Reffett operator using EGM.\n", 318 | "\n", 319 | " \"\"\"\n", 320 | "\n", 321 | " # Simplify names\n", 322 | " u, f, β, μ, ν, s_grid, shocks, α, u_prime, f_prime, u_prime_inv = model\n", 323 | "\n", 324 | " # Linear interpolation of policy on the endogenous grid\n", 325 | " σ = lambda x: np.interp(x, x_in, c_in)\n", 326 | "\n", 327 | " # Allocate memory for new consumption array\n", 328 | " c_out = np.empty_like(s_grid)\n", 329 | "\n", 330 | " for i, s in enumerate(s_grid):\n", 331 | " # Approximate marginal utility ∫ u'(σ(f(s, α)z)) f'(s, α) z ϕ(z)dz\n", 332 | " vals = u_prime(σ(f(s, α) * shocks)) * f_prime(s, α) * shocks\n", 333 | " mu = np.mean(vals)\n", 334 | " # Compute consumption\n", 335 | " c_out[i] = u_prime_inv(β * mu)\n", 336 | "\n", 337 | " # Determine corresponding endogenous grid\n", 338 | " x_out = s_grid + c_out # x_i = s_i + c_i\n", 339 | "\n", 340 | " return c_out, x_out" 341 | ] 342 | }, 343 | { 344 | "cell_type": "markdown", 345 | "id": "fe466432", 346 | "metadata": {}, 347 | "source": [ 348 | "Note the lack of any root-finding algorithm.\n", 349 | "\n", 350 | ">**Note**\n", 351 | ">\n", 352 | ">The routine is still not particularly fast because we are using pure Python loops.\n", 353 | "\n", 354 | "But in the next lecture ([Optimal Savings VI: EGM with JAX](https://python.quantecon.org/os_egm_jax.html)) we will use a fully vectorized and efficient solution." 355 | ] 356 | }, 357 | { 358 | "cell_type": "markdown", 359 | "id": "579858ec", 360 | "metadata": {}, 361 | "source": [ 362 | "### Testing\n", 363 | "\n", 364 | "First we create an instance." 365 | ] 366 | }, 367 | { 368 | "cell_type": "code", 369 | "execution_count": null, 370 | "id": "9cbed337", 371 | "metadata": { 372 | "hide-output": false 373 | }, 374 | "outputs": [], 375 | "source": [ 376 | "# Define utility and production functions with derivatives\n", 377 | "u = lambda c: np.log(c)\n", 378 | "u_prime = lambda c: 1 / c\n", 379 | "u_prime_inv = lambda x: 1 / x\n", 380 | "f = lambda k, α: k**α\n", 381 | "f_prime = lambda k, α: α * k**(α - 1)\n", 382 | "\n", 383 | "model = create_model(u=u, f=f, u_prime=u_prime,\n", 384 | " f_prime=f_prime, u_prime_inv=u_prime_inv)\n", 385 | "s_grid = model.s_grid" 386 | ] 387 | }, 388 | { 389 | "cell_type": "markdown", 390 | "id": "1a533fa4", 391 | "metadata": {}, 392 | "source": [ 393 | "Here’s our solver routine:" 394 | ] 395 | }, 396 | { 397 | "cell_type": "code", 398 | "execution_count": null, 399 | "id": "768c1d30", 400 | "metadata": { 401 | "hide-output": false 402 | }, 403 | "outputs": [], 404 | "source": [ 405 | "def solve_model_time_iter(\n", 406 | " model: Model, # Model details\n", 407 | " c_init: np.ndarray, # initial guess of consumption on EG\n", 408 | " x_init: np.ndarray, # initial guess of endogenous grid\n", 409 | " tol: float = 1e-5, # Error tolerance\n", 410 | " max_iter: int = 1000, # Max number of iterations of K\n", 411 | " verbose: bool = True # If true print output\n", 412 | " ):\n", 413 | " \"\"\"\n", 414 | " Solve the model using time iteration with EGM.\n", 415 | " \"\"\"\n", 416 | " c, x = c_init, x_init\n", 417 | " error = tol + 1\n", 418 | " i = 0\n", 419 | "\n", 420 | " while error > tol and i < max_iter:\n", 421 | " c_new, x_new = K(c, x, model)\n", 422 | " error = np.max(np.abs(c_new - c))\n", 423 | " c, x = c_new, x_new\n", 424 | " i += 1\n", 425 | " if verbose:\n", 426 | " print(f\"Iteration {i}, error = {error}\")\n", 427 | "\n", 428 | " if i == max_iter:\n", 429 | " print(\"Warning: maximum iterations reached\")\n", 430 | "\n", 431 | " return c, x" 432 | ] 433 | }, 434 | { 435 | "cell_type": "markdown", 436 | "id": "be639860", 437 | "metadata": {}, 438 | "source": [ 439 | "Let’s call it:" 440 | ] 441 | }, 442 | { 443 | "cell_type": "code", 444 | "execution_count": null, 445 | "id": "c65496c4", 446 | "metadata": { 447 | "hide-output": false 448 | }, 449 | "outputs": [], 450 | "source": [ 451 | "c_init = np.copy(s_grid)\n", 452 | "x_init = s_grid + c_init\n", 453 | "c, x = solve_model_time_iter(model, c_init, x_init)" 454 | ] 455 | }, 456 | { 457 | "cell_type": "markdown", 458 | "id": "28cf7fad", 459 | "metadata": {}, 460 | "source": [ 461 | "Here is a plot of the resulting policy, compared with the true policy:" 462 | ] 463 | }, 464 | { 465 | "cell_type": "code", 466 | "execution_count": null, 467 | "id": "eeb252ab", 468 | "metadata": { 469 | "hide-output": false 470 | }, 471 | "outputs": [], 472 | "source": [ 473 | "fig, ax = plt.subplots()\n", 474 | "\n", 475 | "ax.plot(x, c, lw=2,\n", 476 | " alpha=0.8, label='approximate policy function')\n", 477 | "\n", 478 | "ax.plot(x, σ_star(x, model.α, model.β), 'k--',\n", 479 | " lw=2, alpha=0.8, label='true policy function')\n", 480 | "\n", 481 | "ax.legend()\n", 482 | "plt.show()" 483 | ] 484 | }, 485 | { 486 | "cell_type": "markdown", 487 | "id": "be9d513f", 488 | "metadata": {}, 489 | "source": [ 490 | "The maximal absolute deviation between the two policies is" 491 | ] 492 | }, 493 | { 494 | "cell_type": "code", 495 | "execution_count": null, 496 | "id": "0504eb26", 497 | "metadata": { 498 | "hide-output": false 499 | }, 500 | "outputs": [], 501 | "source": [ 502 | "np.max(np.abs(c - σ_star(x, model.α, model.β)))" 503 | ] 504 | }, 505 | { 506 | "cell_type": "markdown", 507 | "id": "3efe5a26", 508 | "metadata": {}, 509 | "source": [ 510 | "Here’s the execution time:" 511 | ] 512 | }, 513 | { 514 | "cell_type": "code", 515 | "execution_count": null, 516 | "id": "41a56e6b", 517 | "metadata": { 518 | "hide-output": false 519 | }, 520 | "outputs": [], 521 | "source": [ 522 | "with qe.Timer():\n", 523 | " c, x = solve_model_time_iter(model, c_init, x_init, verbose=False)" 524 | ] 525 | }, 526 | { 527 | "cell_type": "markdown", 528 | "id": "f5535410", 529 | "metadata": {}, 530 | "source": [ 531 | "EGM is faster than time iteration because it avoids numerical root-finding.\n", 532 | "\n", 533 | "Instead, we invert the marginal utility function directly, which is much more efficient.\n", 534 | "\n", 535 | "In [Optimal Savings VI: EGM with JAX](https://python.quantecon.org/os_egm_jax.html), we will use a fully vectorized\n", 536 | "and efficient version of EGM that is also parallelized using JAX.\n", 537 | "\n", 538 | "This provides an extremely fast way to solve the optimal consumption problem we\n", 539 | "have been studying for the last few lectures." 540 | ] 541 | } 542 | ], 543 | "metadata": { 544 | "date": 1764675754.2812982, 545 | "filename": "os_egm.md", 546 | "kernelspec": { 547 | "display_name": "Python", 548 | "language": "python3", 549 | "name": "python3" 550 | }, 551 | "title": "Optimal Savings V: The Endogenous Grid Method" 552 | }, 553 | "nbformat": 4, 554 | "nbformat_minor": 5 555 | } -------------------------------------------------------------------------------- /cake_eating_egm.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "86543124", 6 | "metadata": {}, 7 | "source": [ 8 | "
\n", 9 | " \n", 10 | " \"QuantEcon\"\n", 11 | " \n", 12 | "
" 13 | ] 14 | }, 15 | { 16 | "cell_type": "markdown", 17 | "id": "1544e567", 18 | "metadata": {}, 19 | "source": [ 20 | "# Cake Eating V: The Endogenous Grid Method" 21 | ] 22 | }, 23 | { 24 | "cell_type": "markdown", 25 | "id": "b6bbcdf7", 26 | "metadata": {}, 27 | "source": [ 28 | "## Contents\n", 29 | "\n", 30 | "- [Cake Eating V: The Endogenous Grid Method](#Cake-Eating-V:-The-Endogenous-Grid-Method) \n", 31 | " - [Overview](#Overview) \n", 32 | " - [Key Idea](#Key-Idea) \n", 33 | " - [Implementation](#Implementation) " 34 | ] 35 | }, 36 | { 37 | "cell_type": "markdown", 38 | "id": "af7db430", 39 | "metadata": {}, 40 | "source": [ 41 | "## Overview\n", 42 | "\n", 43 | "Previously, we solved the stochastic cake eating problem using\n", 44 | "\n", 45 | "1. [value function iteration](https://python.quantecon.org/cake_eating_stochastic.html) \n", 46 | "1. [Euler equation based time iteration](https://python.quantecon.org/cake_eating_time_iter.html) \n", 47 | "\n", 48 | "\n", 49 | "We found time iteration to be significantly more accurate and efficient.\n", 50 | "\n", 51 | "In this lecture, we’ll look at a clever twist on time iteration called the **endogenous grid method** (EGM).\n", 52 | "\n", 53 | "EGM is a numerical method for implementing policy iteration invented by [Chris Carroll](https://econ.jhu.edu/directory/christopher-carroll/).\n", 54 | "\n", 55 | "The original reference is [[Carroll, 2006](https://python.quantecon.org/zreferences.html#id168)].\n", 56 | "\n", 57 | "For now we will focus on a clean and simple implementation of EGM that stays\n", 58 | "close to the underlying mathematics.\n", 59 | "\n", 60 | "Then, in [the next lecture](https://python.quantecon.org/cake_eating_egm_jax.html), we will construct a fully vectorized and parallelized version of EGM based on JAX.\n", 61 | "\n", 62 | "Let’s start with some standard imports:" 63 | ] 64 | }, 65 | { 66 | "cell_type": "code", 67 | "execution_count": null, 68 | "id": "cc2f79ed", 69 | "metadata": { 70 | "hide-output": false 71 | }, 72 | "outputs": [], 73 | "source": [ 74 | "import matplotlib.pyplot as plt\n", 75 | "import numpy as np\n", 76 | "import quantecon as qe" 77 | ] 78 | }, 79 | { 80 | "cell_type": "markdown", 81 | "id": "e88c2593", 82 | "metadata": {}, 83 | "source": [ 84 | "## Key Idea\n", 85 | "\n", 86 | "First we remind ourselves of the theory and then we turn to numerical methods." 87 | ] 88 | }, 89 | { 90 | "cell_type": "markdown", 91 | "id": "06deb55b", 92 | "metadata": {}, 93 | "source": [ 94 | "### Theory\n", 95 | "\n", 96 | "We work with the model set out in [Cake Eating IV: Time Iteration](https://python.quantecon.org/cake_eating_time_iter.html), following the same terminology and notation.\n", 97 | "\n", 98 | "The Euler equation is\n", 99 | "\n", 100 | "\n", 101 | "\n", 102 | "$$\n", 103 | "(u'\\circ \\sigma^*)(x)\n", 104 | "= \\beta \\int (u'\\circ \\sigma^*)(f(x - \\sigma^*(x)) z) f'(x - \\sigma^*(x)) z \\phi(dz) \\tag{55.1}\n", 105 | "$$\n", 106 | "\n", 107 | "As we saw, the Coleman-Reffett operator is a nonlinear operator $ K $ engineered so that $ \\sigma^* $ is a fixed point of $ K $.\n", 108 | "\n", 109 | "It takes as its argument a continuous strictly increasing consumption policy $ \\sigma \\in \\Sigma $.\n", 110 | "\n", 111 | "It returns a new function $ K \\sigma $, where $ (K \\sigma)(x) $ is the $ c \\in (0, \\infty) $ that solves\n", 112 | "\n", 113 | "\n", 114 | "\n", 115 | "$$\n", 116 | "u'(c)\n", 117 | "= \\beta \\int (u' \\circ \\sigma) (f(x - c) z ) f'(x - c) z \\phi(dz) \\tag{55.2}\n", 118 | "$$" 119 | ] 120 | }, 121 | { 122 | "cell_type": "markdown", 123 | "id": "c1df4345", 124 | "metadata": {}, 125 | "source": [ 126 | "### Exogenous Grid\n", 127 | "\n", 128 | "As discussed in [Cake Eating IV: Time Iteration](https://python.quantecon.org/cake_eating_time_iter.html), to implement the method on a\n", 129 | "computer, we need numerical approximation.\n", 130 | "\n", 131 | "In particular, we represent a policy function by a set of values on a finite grid.\n", 132 | "\n", 133 | "The function itself is reconstructed from this representation when necessary,\n", 134 | "using interpolation or some other method.\n", 135 | "\n", 136 | "Our [previous strategy](https://python.quantecon.org/cake_eating_time_iter.html) for obtaining a finite representation of an updated consumption policy was to\n", 137 | "\n", 138 | "- fix a grid of income points $ \\{x_i\\} $ \n", 139 | "- calculate the consumption value $ c_i $ corresponding to each $ x_i $ using\n", 140 | " [(55.2)](#equation-egm-coledef) and a root-finding routine \n", 141 | "\n", 142 | "\n", 143 | "Each $ c_i $ is then interpreted as the value of the function $ K \\sigma $ at $ x_i $.\n", 144 | "\n", 145 | "Thus, with the pairs $ \\{(x_i, c_i)\\} $ in hand, we can reconstruct $ K \\sigma $ via approximation.\n", 146 | "\n", 147 | "Iteration then continues…" 148 | ] 149 | }, 150 | { 151 | "cell_type": "markdown", 152 | "id": "7729d35f", 153 | "metadata": {}, 154 | "source": [ 155 | "### Endogenous Grid\n", 156 | "\n", 157 | "The method discussed above requires a root-finding routine to find the\n", 158 | "$ c_i $ corresponding to a given income value $ x_i $.\n", 159 | "\n", 160 | "Root-finding is costly because it typically involves a significant number of\n", 161 | "function evaluations.\n", 162 | "\n", 163 | "As pointed out by Carroll [[Carroll, 2006](https://python.quantecon.org/zreferences.html#id168)], we can avoid this step if\n", 164 | "$ x_i $ is chosen endogenously.\n", 165 | "\n", 166 | "The only assumption required is that $ u' $ is invertible on $ (0, \\infty) $.\n", 167 | "\n", 168 | "Let $ (u')^{-1} $ be the inverse function of $ u' $.\n", 169 | "\n", 170 | "The idea is this:\n", 171 | "\n", 172 | "- First, we fix an *exogenous* grid $ \\{s_i\\} $ for savings ($ s = x - c $). \n", 173 | "- Then we obtain $ c_i $ via \n", 174 | "\n", 175 | "\n", 176 | "\n", 177 | "\n", 178 | "$$\n", 179 | "c_i =\n", 180 | "(u')^{-1}\n", 181 | "\\left\\{\n", 182 | " \\beta \\int (u' \\circ \\sigma) (f(s_i) z ) \\, f'(s_i) \\, z \\, \\phi(dz)\n", 183 | "\\right\\} \\tag{55.3}\n", 184 | "$$\n", 185 | "\n", 186 | "- Finally, for each $ c_i $ we set $ x_i = c_i + s_i $. \n", 187 | "\n", 188 | "\n", 189 | "Importantly, each $ (x_i, c_i) $ pair constructed in this manner satisfies [(55.2)](#equation-egm-coledef).\n", 190 | "\n", 191 | "With the points $ \\{x_i, c_i\\} $ in hand, we can reconstruct $ K \\sigma $ via approximation as before.\n", 192 | "\n", 193 | "The name EGM comes from the fact that the grid $ \\{x_i\\} $ is determined **endogenously**." 194 | ] 195 | }, 196 | { 197 | "cell_type": "markdown", 198 | "id": "5a379f63", 199 | "metadata": {}, 200 | "source": [ 201 | "## Implementation\n", 202 | "\n", 203 | "As in [Cake Eating IV: Time Iteration](https://python.quantecon.org/cake_eating_time_iter.html), we will start with a simple setting where\n", 204 | "\n", 205 | "- $ u(c) = \\ln c $, \n", 206 | "- the function $ f $ has a Cobb-Douglas specification, and \n", 207 | "- the shocks are lognormal. \n", 208 | "\n", 209 | "\n", 210 | "This will allow us to make comparisons with the analytical solutions." 211 | ] 212 | }, 213 | { 214 | "cell_type": "code", 215 | "execution_count": null, 216 | "id": "90243391", 217 | "metadata": { 218 | "hide-output": false 219 | }, 220 | "outputs": [], 221 | "source": [ 222 | "def v_star(x, α, β, μ):\n", 223 | " \"\"\"\n", 224 | " True value function\n", 225 | " \"\"\"\n", 226 | " c1 = np.log(1 - α * β) / (1 - β)\n", 227 | " c2 = (μ + α * np.log(α * β)) / (1 - α)\n", 228 | " c3 = 1 / (1 - β)\n", 229 | " c4 = 1 / (1 - α * β)\n", 230 | " return c1 + c2 * (c3 - c4) + c4 * np.log(x)\n", 231 | "\n", 232 | "def σ_star(x, α, β):\n", 233 | " \"\"\"\n", 234 | " True optimal policy\n", 235 | " \"\"\"\n", 236 | " return (1 - α * β) * x" 237 | ] 238 | }, 239 | { 240 | "cell_type": "markdown", 241 | "id": "d85312db", 242 | "metadata": {}, 243 | "source": [ 244 | "We reuse the `Model` structure from [Cake Eating IV: Time Iteration](https://python.quantecon.org/cake_eating_time_iter.html)." 245 | ] 246 | }, 247 | { 248 | "cell_type": "code", 249 | "execution_count": null, 250 | "id": "1e20c204", 251 | "metadata": { 252 | "hide-output": false 253 | }, 254 | "outputs": [], 255 | "source": [ 256 | "from typing import NamedTuple, Callable\n", 257 | "\n", 258 | "class Model(NamedTuple):\n", 259 | " u: Callable # utility function\n", 260 | " f: Callable # production function\n", 261 | " β: float # discount factor\n", 262 | " μ: float # shock location parameter\n", 263 | " ν: float # shock scale parameter\n", 264 | " s_grid: np.ndarray # exogenous savings grid\n", 265 | " shocks: np.ndarray # shock draws\n", 266 | " α: float # production function parameter\n", 267 | " u_prime: Callable # derivative of utility\n", 268 | " f_prime: Callable # derivative of production\n", 269 | " u_prime_inv: Callable # inverse of u_prime\n", 270 | "\n", 271 | "\n", 272 | "def create_model(u: Callable,\n", 273 | " f: Callable,\n", 274 | " β: float = 0.96,\n", 275 | " μ: float = 0.0,\n", 276 | " ν: float = 0.1,\n", 277 | " grid_max: float = 4.0,\n", 278 | " grid_size: int = 120,\n", 279 | " shock_size: int = 250,\n", 280 | " seed: int = 1234,\n", 281 | " α: float = 0.4,\n", 282 | " u_prime: Callable = None,\n", 283 | " f_prime: Callable = None,\n", 284 | " u_prime_inv: Callable = None) -> Model:\n", 285 | " \"\"\"\n", 286 | " Creates an instance of the cake eating model.\n", 287 | " \"\"\"\n", 288 | " # Set up exogenous savings grid\n", 289 | " s_grid = np.linspace(1e-4, grid_max, grid_size)\n", 290 | "\n", 291 | " # Store shocks (with a seed, so results are reproducible)\n", 292 | " np.random.seed(seed)\n", 293 | " shocks = np.exp(μ + ν * np.random.randn(shock_size))\n", 294 | "\n", 295 | " return Model(u, f, β, μ, ν, s_grid, shocks, α, u_prime, f_prime, u_prime_inv)" 296 | ] 297 | }, 298 | { 299 | "cell_type": "markdown", 300 | "id": "c7adeccf", 301 | "metadata": {}, 302 | "source": [ 303 | "### The Operator\n", 304 | "\n", 305 | "Here’s an implementation of $ K $ using EGM as described above." 306 | ] 307 | }, 308 | { 309 | "cell_type": "code", 310 | "execution_count": null, 311 | "id": "51b0e534", 312 | "metadata": { 313 | "hide-output": false 314 | }, 315 | "outputs": [], 316 | "source": [ 317 | "def K(\n", 318 | " c_in: np.ndarray, # Consumption values on the endogenous grid\n", 319 | " x_in: np.ndarray, # Current endogenous grid\n", 320 | " model: Model # Model specification\n", 321 | " ):\n", 322 | " \"\"\"\n", 323 | " An implementation of the Coleman-Reffett operator using EGM.\n", 324 | "\n", 325 | " \"\"\"\n", 326 | "\n", 327 | " # Simplify names\n", 328 | " u, f, β, μ, ν, s_grid, shocks, α, u_prime, f_prime, u_prime_inv = model\n", 329 | "\n", 330 | " # Linear interpolation of policy on the endogenous grid\n", 331 | " σ = lambda x: np.interp(x, x_in, c_in)\n", 332 | "\n", 333 | " # Allocate memory for new consumption array\n", 334 | " c_out = np.empty_like(s_grid)\n", 335 | "\n", 336 | " # Solve for updated consumption value\n", 337 | " for i, s in enumerate(s_grid):\n", 338 | " vals = u_prime(σ(f(s, α) * shocks)) * f_prime(s, α) * shocks\n", 339 | " c_out[i] = u_prime_inv(β * np.mean(vals))\n", 340 | "\n", 341 | " # Determine corresponding endogenous grid\n", 342 | " x_out = s_grid + c_out # x_i = s_i + c_i\n", 343 | "\n", 344 | " return c_out, x_out" 345 | ] 346 | }, 347 | { 348 | "cell_type": "markdown", 349 | "id": "7bfee776", 350 | "metadata": {}, 351 | "source": [ 352 | "Note the lack of any root-finding algorithm.\n", 353 | "\n", 354 | ">**Note**\n", 355 | ">\n", 356 | ">The routine is still not particularly fast because we are using pure Python loops.\n", 357 | "\n", 358 | "But in the next lecture ([Cake Eating VI: EGM with JAX](https://python.quantecon.org/cake_eating_egm_jax.html)) we will use a fully vectorized and efficient solution." 359 | ] 360 | }, 361 | { 362 | "cell_type": "markdown", 363 | "id": "e4ce3f23", 364 | "metadata": {}, 365 | "source": [ 366 | "### Testing\n", 367 | "\n", 368 | "First we create an instance." 369 | ] 370 | }, 371 | { 372 | "cell_type": "code", 373 | "execution_count": null, 374 | "id": "028d9089", 375 | "metadata": { 376 | "hide-output": false 377 | }, 378 | "outputs": [], 379 | "source": [ 380 | "# Define utility and production functions with derivatives\n", 381 | "u = lambda c: np.log(c)\n", 382 | "u_prime = lambda c: 1 / c\n", 383 | "u_prime_inv = lambda x: 1 / x\n", 384 | "f = lambda k, α: k**α\n", 385 | "f_prime = lambda k, α: α * k**(α - 1)\n", 386 | "\n", 387 | "model = create_model(u=u, f=f, u_prime=u_prime,\n", 388 | " f_prime=f_prime, u_prime_inv=u_prime_inv)\n", 389 | "s_grid = model.s_grid" 390 | ] 391 | }, 392 | { 393 | "cell_type": "markdown", 394 | "id": "0879fad0", 395 | "metadata": {}, 396 | "source": [ 397 | "Here’s our solver routine:" 398 | ] 399 | }, 400 | { 401 | "cell_type": "code", 402 | "execution_count": null, 403 | "id": "04fe6dbf", 404 | "metadata": { 405 | "hide-output": false 406 | }, 407 | "outputs": [], 408 | "source": [ 409 | "def solve_model_time_iter(model: Model,\n", 410 | " c_init: np.ndarray,\n", 411 | " x_init: np.ndarray,\n", 412 | " tol: float = 1e-5,\n", 413 | " max_iter: int = 1000,\n", 414 | " verbose: bool = True):\n", 415 | " \"\"\"\n", 416 | " Solve the model using time iteration with EGM.\n", 417 | " \"\"\"\n", 418 | " c, x = c_init, x_init\n", 419 | " error = tol + 1\n", 420 | " i = 0\n", 421 | "\n", 422 | " while error > tol and i < max_iter:\n", 423 | " c_new, x_new = K(c, x, model)\n", 424 | " error = np.max(np.abs(c_new - c))\n", 425 | " c, x = c_new, x_new\n", 426 | " i += 1\n", 427 | " if verbose:\n", 428 | " print(f\"Iteration {i}, error = {error}\")\n", 429 | "\n", 430 | " if i == max_iter:\n", 431 | " print(\"Warning: maximum iterations reached\")\n", 432 | "\n", 433 | " return c, x" 434 | ] 435 | }, 436 | { 437 | "cell_type": "markdown", 438 | "id": "b5374a17", 439 | "metadata": {}, 440 | "source": [ 441 | "Let’s call it:" 442 | ] 443 | }, 444 | { 445 | "cell_type": "code", 446 | "execution_count": null, 447 | "id": "4e1af54e", 448 | "metadata": { 449 | "hide-output": false 450 | }, 451 | "outputs": [], 452 | "source": [ 453 | "c_init = np.copy(s_grid)\n", 454 | "x_init = s_grid + c_init\n", 455 | "c, x = solve_model_time_iter(model, c_init, x_init)" 456 | ] 457 | }, 458 | { 459 | "cell_type": "markdown", 460 | "id": "e4d02cb9", 461 | "metadata": {}, 462 | "source": [ 463 | "Here is a plot of the resulting policy, compared with the true policy:" 464 | ] 465 | }, 466 | { 467 | "cell_type": "code", 468 | "execution_count": null, 469 | "id": "6cc997fd", 470 | "metadata": { 471 | "hide-output": false 472 | }, 473 | "outputs": [], 474 | "source": [ 475 | "fig, ax = plt.subplots()\n", 476 | "\n", 477 | "ax.plot(x, c, lw=2,\n", 478 | " alpha=0.8, label='approximate policy function')\n", 479 | "\n", 480 | "ax.plot(x, σ_star(x, model.α, model.β), 'k--',\n", 481 | " lw=2, alpha=0.8, label='true policy function')\n", 482 | "\n", 483 | "ax.legend()\n", 484 | "plt.show()" 485 | ] 486 | }, 487 | { 488 | "cell_type": "markdown", 489 | "id": "1ce11600", 490 | "metadata": {}, 491 | "source": [ 492 | "The maximal absolute deviation between the two policies is" 493 | ] 494 | }, 495 | { 496 | "cell_type": "code", 497 | "execution_count": null, 498 | "id": "a42290df", 499 | "metadata": { 500 | "hide-output": false 501 | }, 502 | "outputs": [], 503 | "source": [ 504 | "np.max(np.abs(c - σ_star(x, model.α, model.β)))" 505 | ] 506 | }, 507 | { 508 | "cell_type": "markdown", 509 | "id": "bb4764eb", 510 | "metadata": {}, 511 | "source": [ 512 | "Here’s the execution time:" 513 | ] 514 | }, 515 | { 516 | "cell_type": "code", 517 | "execution_count": null, 518 | "id": "2b09a1fc", 519 | "metadata": { 520 | "hide-output": false 521 | }, 522 | "outputs": [], 523 | "source": [ 524 | "with qe.Timer():\n", 525 | " c, x = solve_model_time_iter(model, c_init, x_init, verbose=False)" 526 | ] 527 | }, 528 | { 529 | "cell_type": "markdown", 530 | "id": "092040d6", 531 | "metadata": {}, 532 | "source": [ 533 | "EGM is faster than time iteration because it avoids numerical root-finding.\n", 534 | "\n", 535 | "Instead, we invert the marginal utility function directly, which is much more efficient.\n", 536 | "\n", 537 | "In the [next lecture](https://python.quantecon.org/cake_eating_egm_jax.html), we will use a fully vectorized\n", 538 | "and efficient version of EGM that is also parallelized using JAX.\n", 539 | "\n", 540 | "This provides an extremely fast way to solve the optimal consumption problem we\n", 541 | "have been studying for the last few lectures." 542 | ] 543 | } 544 | ], 545 | "metadata": { 546 | "date": 1763963171.0992913, 547 | "filename": "cake_eating_egm.md", 548 | "kernelspec": { 549 | "display_name": "Python", 550 | "language": "python3", 551 | "name": "python3" 552 | }, 553 | "title": "Cake Eating V: The Endogenous Grid Method" 554 | }, 555 | "nbformat": 4, 556 | "nbformat_minor": 5 557 | } -------------------------------------------------------------------------------- /inventory_dynamics.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "8b2e20dc", 6 | "metadata": {}, 7 | "source": [ 8 | "
\n", 9 | " \n", 10 | " \"QuantEcon\"\n", 11 | " \n", 12 | "
" 13 | ] 14 | }, 15 | { 16 | "cell_type": "markdown", 17 | "id": "6879e1c7", 18 | "metadata": {}, 19 | "source": [ 20 | "# Inventory Dynamics\n", 21 | "\n", 22 | "\n", 23 | "" 24 | ] 25 | }, 26 | { 27 | "cell_type": "markdown", 28 | "id": "ba406dfc", 29 | "metadata": {}, 30 | "source": [ 31 | "## Contents\n", 32 | "\n", 33 | "- [Inventory Dynamics](#Inventory-Dynamics) \n", 34 | " - [Overview](#Overview) \n", 35 | " - [Sample Paths](#Sample-Paths) \n", 36 | " - [Marginal Distributions](#Marginal-Distributions) \n", 37 | " - [Exercises](#Exercises) " 38 | ] 39 | }, 40 | { 41 | "cell_type": "markdown", 42 | "id": "6b4878ba", 43 | "metadata": {}, 44 | "source": [ 45 | "## Overview\n", 46 | "\n", 47 | "In this lecture we will study the time path of inventories for firms that\n", 48 | "follow so-called s-S inventory dynamics.\n", 49 | "\n", 50 | "Such firms\n", 51 | "\n", 52 | "1. wait until inventory falls below some level $ s $ and then \n", 53 | "1. order sufficient quantities to bring their inventory back up to capacity $ S $. \n", 54 | "\n", 55 | "\n", 56 | "These kinds of policies are common in practice and also optimal in certain circumstances.\n", 57 | "\n", 58 | "A review of early literature and some macroeconomic implications can be found in [[Caplin, 1985](https://python.quantecon.org/zreferences.html#id64)].\n", 59 | "\n", 60 | "Here our main aim is to learn more about simulation, time series and Markov dynamics.\n", 61 | "\n", 62 | "While our Markov environment and many of the concepts we consider are related to those found in our [lecture on finite Markov chains](https://python.quantecon.org/finite_markov.html), the state space is a continuum in the current application.\n", 63 | "\n", 64 | "Let’s start with some imports" 65 | ] 66 | }, 67 | { 68 | "cell_type": "code", 69 | "execution_count": null, 70 | "id": "3fbddfab", 71 | "metadata": { 72 | "hide-output": false 73 | }, 74 | "outputs": [], 75 | "source": [ 76 | "import matplotlib.pyplot as plt\n", 77 | "import numpy as np\n", 78 | "from numba import jit, float64, prange\n", 79 | "from numba.experimental import jitclass" 80 | ] 81 | }, 82 | { 83 | "cell_type": "markdown", 84 | "id": "34bf5ce6", 85 | "metadata": {}, 86 | "source": [ 87 | "## Sample Paths\n", 88 | "\n", 89 | "Consider a firm with inventory $ X_t $.\n", 90 | "\n", 91 | "The firm waits until $ X_t \\leq s $ and then restocks up to $ S $ units.\n", 92 | "\n", 93 | "It faces stochastic demand $ \\{ D_t \\} $, which we assume is IID.\n", 94 | "\n", 95 | "With notation $ a^+ := \\max\\{a, 0\\} $, inventory dynamics can be written\n", 96 | "as\n", 97 | "\n", 98 | "$$\n", 99 | "X_{t+1} =\n", 100 | " \\begin{cases}\n", 101 | " ( S - D_{t+1})^+ & \\quad \\text{if } X_t \\leq s \\\\\n", 102 | " ( X_t - D_{t+1} )^+ & \\quad \\text{if } X_t > s\n", 103 | " \\end{cases}\n", 104 | "$$\n", 105 | "\n", 106 | "In what follows, we will assume that each $ D_t $ is lognormal, so that\n", 107 | "\n", 108 | "$$\n", 109 | "D_t = \\exp(\\mu + \\sigma Z_t)\n", 110 | "$$\n", 111 | "\n", 112 | "where $ \\mu $ and $ \\sigma $ are parameters and $ \\{Z_t\\} $ is IID\n", 113 | "and standard normal.\n", 114 | "\n", 115 | "Here’s a class that stores parameters and generates time paths for inventory." 116 | ] 117 | }, 118 | { 119 | "cell_type": "code", 120 | "execution_count": null, 121 | "id": "38741e53", 122 | "metadata": { 123 | "hide-output": false 124 | }, 125 | "outputs": [], 126 | "source": [ 127 | "firm_data = [\n", 128 | " ('s', float64), # restock trigger level\n", 129 | " ('S', float64), # capacity\n", 130 | " ('mu', float64), # shock location parameter\n", 131 | " ('sigma', float64) # shock scale parameter\n", 132 | "]\n", 133 | "\n", 134 | "\n", 135 | "@jitclass(firm_data)\n", 136 | "class Firm:\n", 137 | "\n", 138 | " def __init__(self, s=10, S=100, mu=1.0, sigma=0.5):\n", 139 | "\n", 140 | " self.s, self.S, self.mu, self.sigma = s, S, mu, sigma\n", 141 | "\n", 142 | " def update(self, x):\n", 143 | " \"Update the state from t to t+1 given current state x.\"\n", 144 | "\n", 145 | " Z = np.random.randn()\n", 146 | " D = np.exp(self.mu + self.sigma * Z)\n", 147 | " if x <= self.s:\n", 148 | " return max(self.S - D, 0)\n", 149 | " else:\n", 150 | " return max(x - D, 0)\n", 151 | "\n", 152 | " def sim_inventory_path(self, x_init, sim_length):\n", 153 | "\n", 154 | " X = np.empty(sim_length)\n", 155 | " X[0] = x_init\n", 156 | "\n", 157 | " for t in range(sim_length-1):\n", 158 | " X[t+1] = self.update(X[t])\n", 159 | " return X" 160 | ] 161 | }, 162 | { 163 | "cell_type": "markdown", 164 | "id": "52ce9ff3", 165 | "metadata": {}, 166 | "source": [ 167 | "Let’s run a first simulation, of a single path:" 168 | ] 169 | }, 170 | { 171 | "cell_type": "code", 172 | "execution_count": null, 173 | "id": "986aff6a", 174 | "metadata": { 175 | "hide-output": false 176 | }, 177 | "outputs": [], 178 | "source": [ 179 | "firm = Firm()\n", 180 | "\n", 181 | "s, S = firm.s, firm.S\n", 182 | "sim_length = 100\n", 183 | "x_init = 50\n", 184 | "\n", 185 | "X = firm.sim_inventory_path(x_init, sim_length)\n", 186 | "\n", 187 | "fig, ax = plt.subplots()\n", 188 | "bbox = (0., 1.02, 1., .102)\n", 189 | "legend_args = {'ncol': 3,\n", 190 | " 'bbox_to_anchor': bbox,\n", 191 | " 'loc': 3,\n", 192 | " 'mode': 'expand'}\n", 193 | "\n", 194 | "ax.plot(X, label=\"inventory\")\n", 195 | "ax.plot(np.full(sim_length, s), 'k--', label=\"$s$\")\n", 196 | "ax.plot(np.full(sim_length, S), 'k-', label=\"$S$\")\n", 197 | "ax.set_ylim(0, S+10)\n", 198 | "ax.set_xlabel(\"time\")\n", 199 | "ax.legend(**legend_args)\n", 200 | "\n", 201 | "plt.show()" 202 | ] 203 | }, 204 | { 205 | "cell_type": "markdown", 206 | "id": "a719a238", 207 | "metadata": {}, 208 | "source": [ 209 | "Now let’s simulate multiple paths in order to build a more complete picture of\n", 210 | "the probabilities of different outcomes:" 211 | ] 212 | }, 213 | { 214 | "cell_type": "code", 215 | "execution_count": null, 216 | "id": "b68bb674", 217 | "metadata": { 218 | "hide-output": false 219 | }, 220 | "outputs": [], 221 | "source": [ 222 | "sim_length=200\n", 223 | "fig, ax = plt.subplots()\n", 224 | "\n", 225 | "ax.plot(np.full(sim_length, s), 'k--', label=\"$s$\")\n", 226 | "ax.plot(np.full(sim_length, S), 'k-', label=\"$S$\")\n", 227 | "ax.set_ylim(0, S+10)\n", 228 | "ax.legend(**legend_args)\n", 229 | "\n", 230 | "for i in range(400):\n", 231 | " X = firm.sim_inventory_path(x_init, sim_length)\n", 232 | " ax.plot(X, 'b', alpha=0.2, lw=0.5)\n", 233 | "\n", 234 | "plt.show()" 235 | ] 236 | }, 237 | { 238 | "cell_type": "markdown", 239 | "id": "01140646", 240 | "metadata": {}, 241 | "source": [ 242 | "## Marginal Distributions\n", 243 | "\n", 244 | "Now let’s look at the marginal distribution $ \\psi_T $ of $ X_T $ for some\n", 245 | "fixed $ T $.\n", 246 | "\n", 247 | "We will do this by generating many draws of $ X_T $ given initial\n", 248 | "condition $ X_0 $.\n", 249 | "\n", 250 | "With these draws of $ X_T $ we can build up a picture of its distribution $ \\psi_T $.\n", 251 | "\n", 252 | "Here’s one visualization, with $ T=50 $." 253 | ] 254 | }, 255 | { 256 | "cell_type": "code", 257 | "execution_count": null, 258 | "id": "21fc5128", 259 | "metadata": { 260 | "hide-output": false 261 | }, 262 | "outputs": [], 263 | "source": [ 264 | "T = 50\n", 265 | "M = 200 # Number of draws\n", 266 | "\n", 267 | "ymin, ymax = 0, S + 10\n", 268 | "\n", 269 | "fig, axes = plt.subplots(1, 2, figsize=(11, 6))\n", 270 | "\n", 271 | "for ax in axes:\n", 272 | " ax.grid(alpha=0.4)\n", 273 | "\n", 274 | "ax = axes[0]\n", 275 | "\n", 276 | "ax.set_ylim(ymin, ymax)\n", 277 | "ax.set_ylabel('$X_t$', fontsize=16)\n", 278 | "ax.vlines((T,), -1.5, 1.5)\n", 279 | "\n", 280 | "ax.set_xticks((T,))\n", 281 | "ax.set_xticklabels((r'$T$',))\n", 282 | "\n", 283 | "sample = np.empty(M)\n", 284 | "for m in range(M):\n", 285 | " X = firm.sim_inventory_path(x_init, 2 * T)\n", 286 | " ax.plot(X, 'b-', lw=1, alpha=0.5)\n", 287 | " ax.plot((T,), (X[T+1],), 'ko', alpha=0.5)\n", 288 | " sample[m] = X[T+1]\n", 289 | "\n", 290 | "axes[1].set_ylim(ymin, ymax)\n", 291 | "\n", 292 | "axes[1].hist(sample,\n", 293 | " bins=16,\n", 294 | " density=True,\n", 295 | " orientation='horizontal',\n", 296 | " histtype='bar',\n", 297 | " alpha=0.5)\n", 298 | "\n", 299 | "plt.show()" 300 | ] 301 | }, 302 | { 303 | "cell_type": "markdown", 304 | "id": "dfb59769", 305 | "metadata": {}, 306 | "source": [ 307 | "We can build up a clearer picture by drawing more samples" 308 | ] 309 | }, 310 | { 311 | "cell_type": "code", 312 | "execution_count": null, 313 | "id": "451da43a", 314 | "metadata": { 315 | "hide-output": false 316 | }, 317 | "outputs": [], 318 | "source": [ 319 | "T = 50\n", 320 | "M = 50_000\n", 321 | "\n", 322 | "fig, ax = plt.subplots()\n", 323 | "\n", 324 | "sample = np.empty(M)\n", 325 | "for m in range(M):\n", 326 | " X = firm.sim_inventory_path(x_init, T+1)\n", 327 | " sample[m] = X[T]\n", 328 | "\n", 329 | "ax.hist(sample,\n", 330 | " bins=36,\n", 331 | " density=True,\n", 332 | " histtype='bar',\n", 333 | " alpha=0.75)\n", 334 | "\n", 335 | "plt.show()" 336 | ] 337 | }, 338 | { 339 | "cell_type": "markdown", 340 | "id": "107eba01", 341 | "metadata": {}, 342 | "source": [ 343 | "Note that the distribution is bimodal\n", 344 | "\n", 345 | "- Most firms have restocked twice but a few have restocked only once (see figure with paths above). \n", 346 | "- Firms in the second category have lower inventory. \n", 347 | "\n", 348 | "\n", 349 | "We can also approximate the distribution using a [kernel density estimator](https://en.wikipedia.org/wiki/Kernel_density_estimation).\n", 350 | "\n", 351 | "Kernel density estimators can be thought of as smoothed histograms.\n", 352 | "\n", 353 | "They are preferable to histograms when the distribution being estimated is likely to be smooth.\n", 354 | "\n", 355 | "We will use a kernel density estimator from [scikit-learn](https://scikit-learn.org/stable/)" 356 | ] 357 | }, 358 | { 359 | "cell_type": "code", 360 | "execution_count": null, 361 | "id": "7a49179f", 362 | "metadata": { 363 | "hide-output": false 364 | }, 365 | "outputs": [], 366 | "source": [ 367 | "from sklearn.neighbors import KernelDensity\n", 368 | "\n", 369 | "def plot_kde(sample, ax, label=''):\n", 370 | "\n", 371 | " xmin, xmax = 0.9 * min(sample), 1.1 * max(sample)\n", 372 | " xgrid = np.linspace(xmin, xmax, 200)\n", 373 | " kde = KernelDensity(kernel='gaussian').fit(sample[:, None])\n", 374 | " log_dens = kde.score_samples(xgrid[:, None])\n", 375 | "\n", 376 | " ax.plot(xgrid, np.exp(log_dens), label=label)" 377 | ] 378 | }, 379 | { 380 | "cell_type": "code", 381 | "execution_count": null, 382 | "id": "76b6d9c3", 383 | "metadata": { 384 | "hide-output": false 385 | }, 386 | "outputs": [], 387 | "source": [ 388 | "fig, ax = plt.subplots()\n", 389 | "plot_kde(sample, ax)\n", 390 | "plt.show()" 391 | ] 392 | }, 393 | { 394 | "cell_type": "markdown", 395 | "id": "42ac5b97", 396 | "metadata": {}, 397 | "source": [ 398 | "The allocation of probability mass is similar to what was shown by the\n", 399 | "histogram just above." 400 | ] 401 | }, 402 | { 403 | "cell_type": "markdown", 404 | "id": "7d4567ba", 405 | "metadata": {}, 406 | "source": [ 407 | "## Exercises" 408 | ] 409 | }, 410 | { 411 | "cell_type": "markdown", 412 | "id": "288e4001", 413 | "metadata": {}, 414 | "source": [ 415 | "## Exercise 35.1\n", 416 | "\n", 417 | "This model is asymptotically stationary, with a unique stationary\n", 418 | "distribution.\n", 419 | "\n", 420 | "(See the discussion of stationarity in [our lecture on AR(1) processes](https://intro.quantecon.org/ar1_processes.html) for background — the fundamental concepts are the same.)\n", 421 | "\n", 422 | "In particular, the sequence of marginal distributions $ \\{\\psi_t\\} $\n", 423 | "is converging to a unique limiting distribution that does not depend on\n", 424 | "initial conditions.\n", 425 | "\n", 426 | "Although we will not prove this here, we can investigate it using simulation.\n", 427 | "\n", 428 | "Your task is to generate and plot the sequence $ \\{\\psi_t\\} $ at times\n", 429 | "$ t = 10, 50, 250, 500, 750 $ based on the discussion above.\n", 430 | "\n", 431 | "(The kernel density estimator is probably the best way to present each\n", 432 | "distribution.)\n", 433 | "\n", 434 | "You should see convergence, in the sense that differences between successive distributions are getting smaller.\n", 435 | "\n", 436 | "Try different initial conditions to verify that, in the long run, the distribution is invariant across initial conditions." 437 | ] 438 | }, 439 | { 440 | "cell_type": "markdown", 441 | "id": "2ac93f09", 442 | "metadata": {}, 443 | "source": [ 444 | "## Solution\n", 445 | "\n", 446 | "Below is one possible solution:\n", 447 | "\n", 448 | "The computations involve a lot of CPU cycles so we have tried to write the\n", 449 | "code efficiently.\n", 450 | "\n", 451 | "This meant writing a specialized function rather than using the class above." 452 | ] 453 | }, 454 | { 455 | "cell_type": "code", 456 | "execution_count": null, 457 | "id": "0202c935", 458 | "metadata": { 459 | "hide-output": false 460 | }, 461 | "outputs": [], 462 | "source": [ 463 | "s, S, mu, sigma = firm.s, firm.S, firm.mu, firm.sigma\n", 464 | "\n", 465 | "@jit(parallel=True)\n", 466 | "def shift_firms_forward(current_inventory_levels, num_periods):\n", 467 | "\n", 468 | " num_firms = len(current_inventory_levels)\n", 469 | " new_inventory_levels = np.empty(num_firms)\n", 470 | "\n", 471 | " for f in prange(num_firms):\n", 472 | " x = current_inventory_levels[f]\n", 473 | " for t in range(num_periods):\n", 474 | " Z = np.random.randn()\n", 475 | " D = np.exp(mu + sigma * Z)\n", 476 | " if x <= s:\n", 477 | " x = max(S - D, 0)\n", 478 | " else:\n", 479 | " x = max(x - D, 0)\n", 480 | " new_inventory_levels[f] = x\n", 481 | "\n", 482 | " return new_inventory_levels" 483 | ] 484 | }, 485 | { 486 | "cell_type": "code", 487 | "execution_count": null, 488 | "id": "aff4aa7a", 489 | "metadata": { 490 | "hide-output": false 491 | }, 492 | "outputs": [], 493 | "source": [ 494 | "x_init = 50\n", 495 | "num_firms = 50_000\n", 496 | "\n", 497 | "sample_dates = 0, 10, 50, 250, 500, 750\n", 498 | "\n", 499 | "first_diffs = np.diff(sample_dates)\n", 500 | "\n", 501 | "fig, ax = plt.subplots()\n", 502 | "\n", 503 | "X = np.full(num_firms, x_init)\n", 504 | "\n", 505 | "current_date = 0\n", 506 | "for d in first_diffs:\n", 507 | " X = shift_firms_forward(X, d)\n", 508 | " current_date += d\n", 509 | " plot_kde(X, ax, label=f't = {current_date}')\n", 510 | "\n", 511 | "ax.set_xlabel('inventory')\n", 512 | "ax.set_ylabel('probability')\n", 513 | "ax.legend()\n", 514 | "plt.show()" 515 | ] 516 | }, 517 | { 518 | "cell_type": "markdown", 519 | "id": "1b857ce1", 520 | "metadata": {}, 521 | "source": [ 522 | "Notice that by $ t=500 $ or $ t=750 $ the densities are barely\n", 523 | "changing.\n", 524 | "\n", 525 | "We have reached a reasonable approximation of the stationary density.\n", 526 | "\n", 527 | "You can convince yourself that initial conditions don’t matter by\n", 528 | "testing a few of them.\n", 529 | "\n", 530 | "For example, try rerunning the code above with all firms starting at\n", 531 | "$ X_0 = 20 $ or $ X_0 = 80 $." 532 | ] 533 | }, 534 | { 535 | "cell_type": "markdown", 536 | "id": "eafdb95a", 537 | "metadata": {}, 538 | "source": [ 539 | "## Exercise 35.2\n", 540 | "\n", 541 | "Using simulation, calculate the probability that firms that start with\n", 542 | "$ X_0 = 70 $ need to order twice or more in the first 50 periods.\n", 543 | "\n", 544 | "You will need a large sample size to get an accurate reading." 545 | ] 546 | }, 547 | { 548 | "cell_type": "markdown", 549 | "id": "49bd44b3", 550 | "metadata": {}, 551 | "source": [ 552 | "## Solution\n", 553 | "\n", 554 | "Here is one solution.\n", 555 | "\n", 556 | "Again, the computations are relatively intensive so we have written a a\n", 557 | "specialized function rather than using the class above.\n", 558 | "\n", 559 | "We will also use parallelization across firms." 560 | ] 561 | }, 562 | { 563 | "cell_type": "code", 564 | "execution_count": null, 565 | "id": "69bdd3ec", 566 | "metadata": { 567 | "hide-output": false 568 | }, 569 | "outputs": [], 570 | "source": [ 571 | "@jit(parallel=True)\n", 572 | "def compute_freq(sim_length=50, x_init=70, num_firms=1_000_000):\n", 573 | "\n", 574 | " firm_counter = 0 # Records number of firms that restock 2x or more\n", 575 | " for m in prange(num_firms):\n", 576 | " x = x_init\n", 577 | " restock_counter = 0 # Will record number of restocks for firm m\n", 578 | "\n", 579 | " for t in range(sim_length):\n", 580 | " Z = np.random.randn()\n", 581 | " D = np.exp(mu + sigma * Z)\n", 582 | " if x <= s:\n", 583 | " x = max(S - D, 0)\n", 584 | " restock_counter += 1\n", 585 | " else:\n", 586 | " x = max(x - D, 0)\n", 587 | "\n", 588 | " if restock_counter > 1:\n", 589 | " firm_counter += 1\n", 590 | "\n", 591 | " return firm_counter / num_firms" 592 | ] 593 | }, 594 | { 595 | "cell_type": "markdown", 596 | "id": "828dfa0a", 597 | "metadata": {}, 598 | "source": [ 599 | "Note the time the routine takes to run, as well as the output." 600 | ] 601 | }, 602 | { 603 | "cell_type": "code", 604 | "execution_count": null, 605 | "id": "c329548b", 606 | "metadata": { 607 | "hide-output": false 608 | }, 609 | "outputs": [], 610 | "source": [ 611 | "%%time\n", 612 | "\n", 613 | "freq = compute_freq()\n", 614 | "print(f\"Frequency of at least two stock outs = {freq}\")" 615 | ] 616 | }, 617 | { 618 | "cell_type": "markdown", 619 | "id": "0edec990", 620 | "metadata": {}, 621 | "source": [ 622 | "Try switching the `parallel` flag to `False` in the jitted function\n", 623 | "above.\n", 624 | "\n", 625 | "Depending on your system, the difference can be substantial.\n", 626 | "\n", 627 | "(On our desktop machine, the speed up is by a factor of 5.)" 628 | ] 629 | } 630 | ], 631 | "metadata": { 632 | "date": 1764675750.4582036, 633 | "filename": "inventory_dynamics.md", 634 | "kernelspec": { 635 | "display_name": "Python", 636 | "language": "python3", 637 | "name": "python3" 638 | }, 639 | "title": "Inventory Dynamics" 640 | }, 641 | "nbformat": 4, 642 | "nbformat_minor": 5 643 | } -------------------------------------------------------------------------------- /cake_eating_egm_jax.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "cde8d038", 6 | "metadata": {}, 7 | "source": [ 8 | "
\n", 9 | " \n", 10 | " \"QuantEcon\"\n", 11 | " \n", 12 | "
" 13 | ] 14 | }, 15 | { 16 | "cell_type": "markdown", 17 | "id": "4e934218", 18 | "metadata": {}, 19 | "source": [ 20 | "# Cake Eating VI: EGM with JAX" 21 | ] 22 | }, 23 | { 24 | "cell_type": "markdown", 25 | "id": "6878f849", 26 | "metadata": {}, 27 | "source": [ 28 | "## Contents\n", 29 | "\n", 30 | "- [Cake Eating VI: EGM with JAX](#Cake-Eating-VI:-EGM-with-JAX) \n", 31 | " - [Overview](#Overview) \n", 32 | " - [Implementation](#Implementation) \n", 33 | " - [Exercises](#Exercises) " 34 | ] 35 | }, 36 | { 37 | "cell_type": "markdown", 38 | "id": "98abb649", 39 | "metadata": {}, 40 | "source": [ 41 | "## Overview\n", 42 | "\n", 43 | "In this lecture, we’ll implement the endogenous grid method (EGM) using JAX.\n", 44 | "\n", 45 | "This lecture builds on [Cake Eating V: The Endogenous Grid Method](https://python.quantecon.org/cake_eating_egm.html), which introduced EGM using NumPy.\n", 46 | "\n", 47 | "By converting to JAX, we can leverage fast linear algebra, hardware accelerators, and JIT compilation for improved performance.\n", 48 | "\n", 49 | "We’ll also use JAX’s `vmap` function to fully vectorize the Coleman-Reffett operator.\n", 50 | "\n", 51 | "Let’s start with some standard imports:" 52 | ] 53 | }, 54 | { 55 | "cell_type": "code", 56 | "execution_count": null, 57 | "id": "72275424", 58 | "metadata": { 59 | "hide-output": false 60 | }, 61 | "outputs": [], 62 | "source": [ 63 | "import matplotlib.pyplot as plt\n", 64 | "import jax\n", 65 | "import jax.numpy as jnp\n", 66 | "import quantecon as qe\n", 67 | "from typing import NamedTuple" 68 | ] 69 | }, 70 | { 71 | "cell_type": "markdown", 72 | "id": "719b4392", 73 | "metadata": {}, 74 | "source": [ 75 | "## Implementation\n", 76 | "\n", 77 | "For details on the savings problem and the endogenous grid method (EGM), please see [Cake Eating V: The Endogenous Grid Method](https://python.quantecon.org/cake_eating_egm.html).\n", 78 | "\n", 79 | "Here we focus on the JAX implementation of EGM.\n", 80 | "\n", 81 | "We use the same setting as in [Cake Eating V: The Endogenous Grid Method](https://python.quantecon.org/cake_eating_egm.html):\n", 82 | "\n", 83 | "- $ u(c) = \\ln c $, \n", 84 | "- production is Cobb-Douglas, and \n", 85 | "- the shocks are lognormal. \n", 86 | "\n", 87 | "\n", 88 | "Here are the analytical solutions for comparison." 89 | ] 90 | }, 91 | { 92 | "cell_type": "code", 93 | "execution_count": null, 94 | "id": "979cce3c", 95 | "metadata": { 96 | "hide-output": false 97 | }, 98 | "outputs": [], 99 | "source": [ 100 | "def v_star(x, α, β, μ):\n", 101 | " \"\"\"\n", 102 | " True value function\n", 103 | " \"\"\"\n", 104 | " c1 = jnp.log(1 - α * β) / (1 - β)\n", 105 | " c2 = (μ + α * jnp.log(α * β)) / (1 - α)\n", 106 | " c3 = 1 / (1 - β)\n", 107 | " c4 = 1 / (1 - α * β)\n", 108 | " return c1 + c2 * (c3 - c4) + c4 * jnp.log(x)\n", 109 | "\n", 110 | "def σ_star(x, α, β):\n", 111 | " \"\"\"\n", 112 | " True optimal policy\n", 113 | " \"\"\"\n", 114 | " return (1 - α * β) * x" 115 | ] 116 | }, 117 | { 118 | "cell_type": "markdown", 119 | "id": "e3f3a354", 120 | "metadata": {}, 121 | "source": [ 122 | "The `Model` class stores only the data (grids, shocks, and parameters).\n", 123 | "\n", 124 | "Utility and production functions will be defined globally to work with JAX’s JIT compiler." 125 | ] 126 | }, 127 | { 128 | "cell_type": "code", 129 | "execution_count": null, 130 | "id": "5eac5b66", 131 | "metadata": { 132 | "hide-output": false 133 | }, 134 | "outputs": [], 135 | "source": [ 136 | "class Model(NamedTuple):\n", 137 | " β: float # discount factor\n", 138 | " μ: float # shock location parameter\n", 139 | " s: float # shock scale parameter\n", 140 | " s_grid: jnp.ndarray # exogenous savings grid\n", 141 | " shocks: jnp.ndarray # shock draws\n", 142 | " α: float # production function parameter\n", 143 | "\n", 144 | "\n", 145 | "def create_model(β: float = 0.96,\n", 146 | " μ: float = 0.0,\n", 147 | " s: float = 0.1,\n", 148 | " grid_max: float = 4.0,\n", 149 | " grid_size: int = 120,\n", 150 | " shock_size: int = 250,\n", 151 | " seed: int = 1234,\n", 152 | " α: float = 0.4) -> Model:\n", 153 | " \"\"\"\n", 154 | " Creates an instance of the cake eating model.\n", 155 | " \"\"\"\n", 156 | " # Set up exogenous savings grid\n", 157 | " s_grid = jnp.linspace(1e-4, grid_max, grid_size)\n", 158 | "\n", 159 | " # Store shocks (with a seed, so results are reproducible)\n", 160 | " key = jax.random.PRNGKey(seed)\n", 161 | " shocks = jnp.exp(μ + s * jax.random.normal(key, shape=(shock_size,)))\n", 162 | "\n", 163 | " return Model(β=β, μ=μ, s=s, s_grid=s_grid, shocks=shocks, α=α)" 164 | ] 165 | }, 166 | { 167 | "cell_type": "markdown", 168 | "id": "e051404e", 169 | "metadata": {}, 170 | "source": [ 171 | "Here’s the Coleman-Reffett operator using EGM.\n", 172 | "\n", 173 | "The key JAX feature here is `vmap`, which vectorizes the computation over the grid points." 174 | ] 175 | }, 176 | { 177 | "cell_type": "code", 178 | "execution_count": null, 179 | "id": "0e7192a5", 180 | "metadata": { 181 | "hide-output": false 182 | }, 183 | "outputs": [], 184 | "source": [ 185 | "def K(\n", 186 | " c_in: jnp.ndarray, # Consumption values on the endogenous grid\n", 187 | " x_in: jnp.ndarray, # Current endogenous grid\n", 188 | " model: Model # Model specification\n", 189 | " ):\n", 190 | " \"\"\"\n", 191 | " The Coleman-Reffett operator using EGM\n", 192 | "\n", 193 | " \"\"\"\n", 194 | "\n", 195 | " # Simplify names\n", 196 | " β, α = model.β, model.α\n", 197 | " s_grid, shocks = model.s_grid, model.shocks\n", 198 | "\n", 199 | " # Linear interpolation of policy using endogenous grid\n", 200 | " σ = lambda x_val: jnp.interp(x_val, x_in, c_in)\n", 201 | "\n", 202 | " # Define function to compute consumption at a single grid point\n", 203 | " def compute_c(s):\n", 204 | " vals = u_prime(σ(f(s, α) * shocks)) * f_prime(s, α) * shocks\n", 205 | " return u_prime_inv(β * jnp.mean(vals))\n", 206 | "\n", 207 | " # Vectorize over grid using vmap\n", 208 | " compute_c_vectorized = jax.vmap(compute_c)\n", 209 | " c_out = compute_c_vectorized(s_grid)\n", 210 | "\n", 211 | " # Determine corresponding endogenous grid\n", 212 | " x_out = s_grid + c_out # x_i = s_i + c_i\n", 213 | "\n", 214 | " return c_out, x_out" 215 | ] 216 | }, 217 | { 218 | "cell_type": "markdown", 219 | "id": "667a9cbd", 220 | "metadata": {}, 221 | "source": [ 222 | "We define utility and production functions globally.\n", 223 | "\n", 224 | "Note that `f` and `f_prime` take `α` as an explicit argument, allowing them to work with JAX’s functional programming model." 225 | ] 226 | }, 227 | { 228 | "cell_type": "code", 229 | "execution_count": null, 230 | "id": "a91333a8", 231 | "metadata": { 232 | "hide-output": false 233 | }, 234 | "outputs": [], 235 | "source": [ 236 | "# Define utility and production functions with derivatives\n", 237 | "u = lambda c: jnp.log(c)\n", 238 | "u_prime = lambda c: 1 / c\n", 239 | "u_prime_inv = lambda x: 1 / x\n", 240 | "f = lambda k, α: k**α\n", 241 | "f_prime = lambda k, α: α * k**(α - 1)" 242 | ] 243 | }, 244 | { 245 | "cell_type": "markdown", 246 | "id": "073ab406", 247 | "metadata": {}, 248 | "source": [ 249 | "Now we create a model instance." 250 | ] 251 | }, 252 | { 253 | "cell_type": "code", 254 | "execution_count": null, 255 | "id": "f5d37ec2", 256 | "metadata": { 257 | "hide-output": false 258 | }, 259 | "outputs": [], 260 | "source": [ 261 | "model = create_model()\n", 262 | "s_grid = model.s_grid" 263 | ] 264 | }, 265 | { 266 | "cell_type": "markdown", 267 | "id": "45bb8379", 268 | "metadata": {}, 269 | "source": [ 270 | "The solver uses JAX’s `jax.lax.while_loop` for the iteration and is JIT-compiled for speed." 271 | ] 272 | }, 273 | { 274 | "cell_type": "code", 275 | "execution_count": null, 276 | "id": "1b9e5e28", 277 | "metadata": { 278 | "hide-output": false 279 | }, 280 | "outputs": [], 281 | "source": [ 282 | "@jax.jit\n", 283 | "def solve_model_time_iter(model: Model,\n", 284 | " c_init: jnp.ndarray,\n", 285 | " x_init: jnp.ndarray,\n", 286 | " tol: float = 1e-5,\n", 287 | " max_iter: int = 1000):\n", 288 | " \"\"\"\n", 289 | " Solve the model using time iteration with EGM.\n", 290 | " \"\"\"\n", 291 | "\n", 292 | " def condition(loop_state):\n", 293 | " i, c, x, error = loop_state\n", 294 | " return (error > tol) & (i < max_iter)\n", 295 | "\n", 296 | " def body(loop_state):\n", 297 | " i, c, x, error = loop_state\n", 298 | " c_new, x_new = K(c, x, model)\n", 299 | " error = jnp.max(jnp.abs(c_new - c))\n", 300 | " return i + 1, c_new, x_new, error\n", 301 | "\n", 302 | " # Initialize loop state\n", 303 | " initial_state = (0, c_init, x_init, tol + 1)\n", 304 | "\n", 305 | " # Run the loop\n", 306 | " i, c, x, error = jax.lax.while_loop(condition, body, initial_state)\n", 307 | "\n", 308 | " return c, x" 309 | ] 310 | }, 311 | { 312 | "cell_type": "markdown", 313 | "id": "029c3e00", 314 | "metadata": {}, 315 | "source": [ 316 | "We solve the model starting from an initial guess." 317 | ] 318 | }, 319 | { 320 | "cell_type": "code", 321 | "execution_count": null, 322 | "id": "8dafc613", 323 | "metadata": { 324 | "hide-output": false 325 | }, 326 | "outputs": [], 327 | "source": [ 328 | "c_init = jnp.copy(s_grid)\n", 329 | "x_init = s_grid + c_init\n", 330 | "c, x = solve_model_time_iter(model, c_init, x_init)" 331 | ] 332 | }, 333 | { 334 | "cell_type": "markdown", 335 | "id": "0bb4b8a5", 336 | "metadata": {}, 337 | "source": [ 338 | "Let’s plot the resulting policy against the analytical solution." 339 | ] 340 | }, 341 | { 342 | "cell_type": "code", 343 | "execution_count": null, 344 | "id": "21447c6d", 345 | "metadata": { 346 | "hide-output": false 347 | }, 348 | "outputs": [], 349 | "source": [ 350 | "fig, ax = plt.subplots()\n", 351 | "\n", 352 | "ax.plot(x, c, lw=2,\n", 353 | " alpha=0.8, label='approximate policy function')\n", 354 | "\n", 355 | "ax.plot(x, σ_star(x, model.α, model.β), 'k--',\n", 356 | " lw=2, alpha=0.8, label='true policy function')\n", 357 | "\n", 358 | "ax.legend()\n", 359 | "plt.show()" 360 | ] 361 | }, 362 | { 363 | "cell_type": "markdown", 364 | "id": "d3ce908a", 365 | "metadata": {}, 366 | "source": [ 367 | "The fit is very good." 368 | ] 369 | }, 370 | { 371 | "cell_type": "code", 372 | "execution_count": null, 373 | "id": "bc759dc8", 374 | "metadata": { 375 | "hide-output": false 376 | }, 377 | "outputs": [], 378 | "source": [ 379 | "max_dev = jnp.max(jnp.abs(c - σ_star(x, model.α, model.β)))\n", 380 | "print(f\"Maximum absolute deviation: {max_dev:.7}\")" 381 | ] 382 | }, 383 | { 384 | "cell_type": "markdown", 385 | "id": "0a0ea980", 386 | "metadata": {}, 387 | "source": [ 388 | "The JAX implementation is very fast thanks to JIT compilation and vectorization." 389 | ] 390 | }, 391 | { 392 | "cell_type": "code", 393 | "execution_count": null, 394 | "id": "86e34557", 395 | "metadata": { 396 | "hide-output": false 397 | }, 398 | "outputs": [], 399 | "source": [ 400 | "with qe.Timer(precision=8):\n", 401 | " c, x = solve_model_time_iter(model, c_init, x_init)\n", 402 | " jax.block_until_ready(c)" 403 | ] 404 | }, 405 | { 406 | "cell_type": "markdown", 407 | "id": "b6ebb1a2", 408 | "metadata": {}, 409 | "source": [ 410 | "This speed comes from:\n", 411 | "\n", 412 | "- JIT compilation of the entire solver \n", 413 | "- Vectorization via `vmap` in the Coleman-Reffett operator \n", 414 | "- Use of `jax.lax.while_loop` instead of a Python loop \n", 415 | "- Efficient JAX array operations throughout " 416 | ] 417 | }, 418 | { 419 | "cell_type": "markdown", 420 | "id": "c83b0f22", 421 | "metadata": {}, 422 | "source": [ 423 | "## Exercises" 424 | ] 425 | }, 426 | { 427 | "cell_type": "markdown", 428 | "id": "7bba0afb", 429 | "metadata": {}, 430 | "source": [ 431 | "## Exercise 56.1\n", 432 | "\n", 433 | "Solve the stochastic cake eating problem with CRRA utility\n", 434 | "\n", 435 | "$$\n", 436 | "u(c) = \\frac{c^{1 - \\gamma} - 1}{1 - \\gamma}\n", 437 | "$$\n", 438 | "\n", 439 | "Compare the optimal policies for values of $ \\gamma $ approaching 1 from above (e.g., 1.05, 1.1, 1.2).\n", 440 | "\n", 441 | "Show that as $ \\gamma \\to 1 $, the optimal policy converges to the policy obtained with log utility ($ \\gamma = 1 $).\n", 442 | "\n", 443 | "Hint: Use values of $ \\gamma $ close to 1 to ensure the endogenous grids have similar coverage and make visual comparison easier." 444 | ] 445 | }, 446 | { 447 | "cell_type": "markdown", 448 | "id": "162e018c", 449 | "metadata": {}, 450 | "source": [ 451 | "## Solution\n", 452 | "\n", 453 | "We need to create a version of the Coleman-Reffett operator and solver that work with CRRA utility.\n", 454 | "\n", 455 | "The key is to parameterize the utility functions by $ \\gamma $." 456 | ] 457 | }, 458 | { 459 | "cell_type": "code", 460 | "execution_count": null, 461 | "id": "e0541215", 462 | "metadata": { 463 | "hide-output": false 464 | }, 465 | "outputs": [], 466 | "source": [ 467 | "def u_crra(c, γ):\n", 468 | " return (c**(1 - γ) - 1) / (1 - γ)\n", 469 | "\n", 470 | "def u_prime_crra(c, γ):\n", 471 | " return c**(-γ)\n", 472 | "\n", 473 | "def u_prime_inv_crra(x, γ):\n", 474 | " return x**(-1/γ)" 475 | ] 476 | }, 477 | { 478 | "cell_type": "markdown", 479 | "id": "a96c8d41", 480 | "metadata": {}, 481 | "source": [ 482 | "Now we create a version of the Coleman-Reffett operator that takes $ \\gamma $ as a parameter." 483 | ] 484 | }, 485 | { 486 | "cell_type": "code", 487 | "execution_count": null, 488 | "id": "733a63a8", 489 | "metadata": { 490 | "hide-output": false 491 | }, 492 | "outputs": [], 493 | "source": [ 494 | "def K_crra(\n", 495 | " c_in: jnp.ndarray, # Consumption values on the endogenous grid\n", 496 | " x_in: jnp.ndarray, # Current endogenous grid\n", 497 | " model: Model, # Model specification\n", 498 | " γ: float # CRRA parameter\n", 499 | " ):\n", 500 | " \"\"\"\n", 501 | " The Coleman-Reffett operator using EGM with CRRA utility\n", 502 | " \"\"\"\n", 503 | " # Simplify names\n", 504 | " β, α = model.β, model.α\n", 505 | " s_grid, shocks = model.s_grid, model.shocks\n", 506 | "\n", 507 | " # Linear interpolation of policy using endogenous grid\n", 508 | " σ = lambda x_val: jnp.interp(x_val, x_in, c_in)\n", 509 | "\n", 510 | " # Define function to compute consumption at a single grid point\n", 511 | " def compute_c(s):\n", 512 | " vals = u_prime_crra(σ(f(s, α) * shocks), γ) * f_prime(s, α) * shocks\n", 513 | " return u_prime_inv_crra(β * jnp.mean(vals), γ)\n", 514 | "\n", 515 | " # Vectorize over grid using vmap\n", 516 | " compute_c_vectorized = jax.vmap(compute_c)\n", 517 | " c_out = compute_c_vectorized(s_grid)\n", 518 | "\n", 519 | " # Determine corresponding endogenous grid\n", 520 | " x_out = s_grid + c_out # x_i = s_i + c_i\n", 521 | "\n", 522 | " return c_out, x_out" 523 | ] 524 | }, 525 | { 526 | "cell_type": "markdown", 527 | "id": "f2b7f7dd", 528 | "metadata": {}, 529 | "source": [ 530 | "We also need a solver that uses this operator." 531 | ] 532 | }, 533 | { 534 | "cell_type": "code", 535 | "execution_count": null, 536 | "id": "5e4b985b", 537 | "metadata": { 538 | "hide-output": false 539 | }, 540 | "outputs": [], 541 | "source": [ 542 | "@jax.jit\n", 543 | "def solve_model_crra(model: Model,\n", 544 | " c_init: jnp.ndarray,\n", 545 | " x_init: jnp.ndarray,\n", 546 | " γ: float,\n", 547 | " tol: float = 1e-5,\n", 548 | " max_iter: int = 1000):\n", 549 | " \"\"\"\n", 550 | " Solve the model using time iteration with EGM and CRRA utility.\n", 551 | " \"\"\"\n", 552 | "\n", 553 | " def condition(loop_state):\n", 554 | " i, c, x, error = loop_state\n", 555 | " return (error > tol) & (i < max_iter)\n", 556 | "\n", 557 | " def body(loop_state):\n", 558 | " i, c, x, error = loop_state\n", 559 | " c_new, x_new = K_crra(c, x, model, γ)\n", 560 | " error = jnp.max(jnp.abs(c_new - c))\n", 561 | " return i + 1, c_new, x_new, error\n", 562 | "\n", 563 | " # Initialize loop state\n", 564 | " initial_state = (0, c_init, x_init, tol + 1)\n", 565 | "\n", 566 | " # Run the loop\n", 567 | " i, c, x, error = jax.lax.while_loop(condition, body, initial_state)\n", 568 | "\n", 569 | " return c, x" 570 | ] 571 | }, 572 | { 573 | "cell_type": "markdown", 574 | "id": "235f8548", 575 | "metadata": {}, 576 | "source": [ 577 | "Now we solve for $ \\gamma = 1 $ (log utility) and values approaching 1 from above." 578 | ] 579 | }, 580 | { 581 | "cell_type": "code", 582 | "execution_count": null, 583 | "id": "320ec3f0", 584 | "metadata": { 585 | "hide-output": false 586 | }, 587 | "outputs": [], 588 | "source": [ 589 | "γ_values = [1.0, 1.05, 1.1, 1.2]\n", 590 | "policies = {}\n", 591 | "endogenous_grids = {}\n", 592 | "\n", 593 | "model_crra = create_model()\n", 594 | "\n", 595 | "for γ in γ_values:\n", 596 | " c_init = jnp.copy(model_crra.s_grid)\n", 597 | " x_init = model_crra.s_grid + c_init\n", 598 | " c_gamma, x_gamma = solve_model_crra(model_crra, c_init, x_init, γ)\n", 599 | " jax.block_until_ready(c_gamma)\n", 600 | " policies[γ] = c_gamma\n", 601 | " endogenous_grids[γ] = x_gamma\n", 602 | " print(f\"Solved for γ = {γ}\")" 603 | ] 604 | }, 605 | { 606 | "cell_type": "markdown", 607 | "id": "255aa755", 608 | "metadata": {}, 609 | "source": [ 610 | "Plot the policies on their endogenous grids." 611 | ] 612 | }, 613 | { 614 | "cell_type": "code", 615 | "execution_count": null, 616 | "id": "14775f57", 617 | "metadata": { 618 | "hide-output": false 619 | }, 620 | "outputs": [], 621 | "source": [ 622 | "fig, ax = plt.subplots()\n", 623 | "\n", 624 | "for γ in γ_values:\n", 625 | " x = endogenous_grids[γ]\n", 626 | " if γ == 1.0:\n", 627 | " ax.plot(x, policies[γ], 'k-', linewidth=2,\n", 628 | " label=f'γ = {γ:.2f} (log utility)', alpha=0.8)\n", 629 | " else:\n", 630 | " ax.plot(x, policies[γ], label=f'γ = {γ:.2f}', alpha=0.8)\n", 631 | "\n", 632 | "ax.set_xlabel('State x')\n", 633 | "ax.set_ylabel('Consumption σ(x)')\n", 634 | "ax.legend()\n", 635 | "ax.set_title('Optimal policies: CRRA utility approaching log case')\n", 636 | "plt.show()" 637 | ] 638 | }, 639 | { 640 | "cell_type": "markdown", 641 | "id": "cbcc4633", 642 | "metadata": {}, 643 | "source": [ 644 | "Note that the plots for $ \\gamma > 1 $ do not cover the entire x-axis range shown.\n", 645 | "\n", 646 | "This is because the endogenous grid $ x = s + \\sigma(s) $ depends on the consumption policy, which varies with $ \\gamma $.\n", 647 | "\n", 648 | "Let’s check the maximum deviation between the log utility case ($ \\gamma = 1.0 $) and values approaching from above." 649 | ] 650 | }, 651 | { 652 | "cell_type": "code", 653 | "execution_count": null, 654 | "id": "de415b12", 655 | "metadata": { 656 | "hide-output": false 657 | }, 658 | "outputs": [], 659 | "source": [ 660 | "for γ in [1.05, 1.1, 1.2]:\n", 661 | " max_diff = jnp.max(jnp.abs(policies[1.0] - policies[γ]))\n", 662 | " print(f\"Max difference between γ=1.0 and γ={γ}: {max_diff:.6}\")" 663 | ] 664 | }, 665 | { 666 | "cell_type": "markdown", 667 | "id": "ea493d6a", 668 | "metadata": {}, 669 | "source": [ 670 | "As expected, the differences decrease as $ \\gamma $ approaches 1 from above, confirming convergence." 671 | ] 672 | } 673 | ], 674 | "metadata": { 675 | "date": 1763963171.1191554, 676 | "filename": "cake_eating_egm_jax.md", 677 | "kernelspec": { 678 | "display_name": "Python", 679 | "language": "python3", 680 | "name": "python3" 681 | }, 682 | "title": "Cake Eating VI: EGM with JAX" 683 | }, 684 | "nbformat": 4, 685 | "nbformat_minor": 5 686 | } -------------------------------------------------------------------------------- /os_egm_jax.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "128799aa", 6 | "metadata": {}, 7 | "source": [ 8 | "
\n", 9 | " \n", 10 | " \"QuantEcon\"\n", 11 | " \n", 12 | "
" 13 | ] 14 | }, 15 | { 16 | "cell_type": "markdown", 17 | "id": "6f11b0a1", 18 | "metadata": {}, 19 | "source": [ 20 | "# Optimal Savings VI: EGM with JAX" 21 | ] 22 | }, 23 | { 24 | "cell_type": "markdown", 25 | "id": "003ec561", 26 | "metadata": {}, 27 | "source": [ 28 | "# GPU\n", 29 | "\n", 30 | "This lecture was built using a machine with access to a GPU.\n", 31 | "\n", 32 | "[Google Colab](https://colab.research.google.com/) has a free tier with GPUs\n", 33 | "that you can access as follows:\n", 34 | "\n", 35 | "1. Click on the “play” icon top right \n", 36 | "1. Select Colab \n", 37 | "1. Set the runtime environment to include a GPU " 38 | ] 39 | }, 40 | { 41 | "cell_type": "markdown", 42 | "id": "ee2ba702", 43 | "metadata": {}, 44 | "source": [ 45 | "## Contents\n", 46 | "\n", 47 | "- [Optimal Savings VI: EGM with JAX](#Optimal-Savings-VI:-EGM-with-JAX) \n", 48 | " - [Overview](#Overview) \n", 49 | " - [Implementation](#Implementation) \n", 50 | " - [Exercises](#Exercises) " 51 | ] 52 | }, 53 | { 54 | "cell_type": "markdown", 55 | "id": "07a90732", 56 | "metadata": {}, 57 | "source": [ 58 | "## Overview\n", 59 | "\n", 60 | "In this lecture, we’ll implement the endogenous grid method (EGM) using JAX.\n", 61 | "\n", 62 | "This lecture builds on [Optimal Savings V: The Endogenous Grid Method](https://python.quantecon.org/os_egm.html), which introduced EGM using NumPy.\n", 63 | "\n", 64 | "By converting to JAX, we can leverage fast linear algebra, hardware accelerators, and JIT compilation for improved performance.\n", 65 | "\n", 66 | "We’ll also use JAX’s `vmap` function to fully vectorize the Coleman-Reffett operator.\n", 67 | "\n", 68 | "Let’s start with some standard imports:" 69 | ] 70 | }, 71 | { 72 | "cell_type": "code", 73 | "execution_count": null, 74 | "id": "7c9cfe71", 75 | "metadata": { 76 | "hide-output": false 77 | }, 78 | "outputs": [], 79 | "source": [ 80 | "import matplotlib.pyplot as plt\n", 81 | "import jax\n", 82 | "import jax.numpy as jnp\n", 83 | "import quantecon as qe\n", 84 | "from typing import NamedTuple" 85 | ] 86 | }, 87 | { 88 | "cell_type": "markdown", 89 | "id": "8846ad8f", 90 | "metadata": {}, 91 | "source": [ 92 | "## Implementation\n", 93 | "\n", 94 | "For details on the savings problem and the endogenous grid method (EGM), please see [Optimal Savings V: The Endogenous Grid Method](https://python.quantecon.org/os_egm.html).\n", 95 | "\n", 96 | "Here we focus on the JAX implementation of EGM.\n", 97 | "\n", 98 | "We use the same setting as in [Optimal Savings V: The Endogenous Grid Method](https://python.quantecon.org/os_egm.html):\n", 99 | "\n", 100 | "- $ u(c) = \\ln c $, \n", 101 | "- production is Cobb-Douglas, and \n", 102 | "- the shocks are lognormal. \n", 103 | "\n", 104 | "\n", 105 | "Here are the analytical solutions for comparison." 106 | ] 107 | }, 108 | { 109 | "cell_type": "code", 110 | "execution_count": null, 111 | "id": "c3ed6941", 112 | "metadata": { 113 | "hide-output": false 114 | }, 115 | "outputs": [], 116 | "source": [ 117 | "def v_star(x, α, β, μ):\n", 118 | " \"\"\"\n", 119 | " True value function\n", 120 | " \"\"\"\n", 121 | " c1 = jnp.log(1 - α * β) / (1 - β)\n", 122 | " c2 = (μ + α * jnp.log(α * β)) / (1 - α)\n", 123 | " c3 = 1 / (1 - β)\n", 124 | " c4 = 1 / (1 - α * β)\n", 125 | " return c1 + c2 * (c3 - c4) + c4 * jnp.log(x)\n", 126 | "\n", 127 | "def σ_star(x, α, β):\n", 128 | " \"\"\"\n", 129 | " True optimal policy\n", 130 | " \"\"\"\n", 131 | " return (1 - α * β) * x" 132 | ] 133 | }, 134 | { 135 | "cell_type": "markdown", 136 | "id": "54754d85", 137 | "metadata": {}, 138 | "source": [ 139 | "The `Model` class stores only the data (grids, shocks, and parameters).\n", 140 | "\n", 141 | "Utility and production functions will be defined globally to work with JAX’s JIT compiler." 142 | ] 143 | }, 144 | { 145 | "cell_type": "code", 146 | "execution_count": null, 147 | "id": "6e731a7f", 148 | "metadata": { 149 | "hide-output": false 150 | }, 151 | "outputs": [], 152 | "source": [ 153 | "class Model(NamedTuple):\n", 154 | " β: float # discount factor\n", 155 | " μ: float # shock location parameter\n", 156 | " s: float # shock scale parameter\n", 157 | " s_grid: jnp.ndarray # exogenous savings grid\n", 158 | " shocks: jnp.ndarray # shock draws\n", 159 | " α: float # production function parameter\n", 160 | "\n", 161 | "\n", 162 | "def create_model(\n", 163 | " β: float = 0.96,\n", 164 | " μ: float = 0.0,\n", 165 | " s: float = 0.1,\n", 166 | " grid_max: float = 4.0,\n", 167 | " grid_size: int = 120,\n", 168 | " shock_size: int = 250,\n", 169 | " seed: int = 1234,\n", 170 | " α: float = 0.4\n", 171 | " ) -> Model:\n", 172 | " \"\"\"\n", 173 | " Creates an instance of the optimal savings model.\n", 174 | " \"\"\"\n", 175 | " # Set up exogenous savings grid\n", 176 | " s_grid = jnp.linspace(1e-4, grid_max, grid_size)\n", 177 | "\n", 178 | " # Store shocks (with a seed, so results are reproducible)\n", 179 | " key = jax.random.PRNGKey(seed)\n", 180 | " shocks = jnp.exp(μ + s * jax.random.normal(key, shape=(shock_size,)))\n", 181 | "\n", 182 | " return Model(β, μ, s, s_grid, shocks, α)" 183 | ] 184 | }, 185 | { 186 | "cell_type": "markdown", 187 | "id": "38ceb134", 188 | "metadata": {}, 189 | "source": [ 190 | "We define utility and production functions globally." 191 | ] 192 | }, 193 | { 194 | "cell_type": "code", 195 | "execution_count": null, 196 | "id": "ffe1c6f6", 197 | "metadata": { 198 | "hide-output": false 199 | }, 200 | "outputs": [], 201 | "source": [ 202 | "# Define utility and production functions with derivatives\n", 203 | "u = lambda c: jnp.log(c)\n", 204 | "u_prime = lambda c: 1 / c\n", 205 | "u_prime_inv = lambda x: 1 / x\n", 206 | "f = lambda k, α: k**α\n", 207 | "f_prime = lambda k, α: α * k**(α - 1)" 208 | ] 209 | }, 210 | { 211 | "cell_type": "markdown", 212 | "id": "6f03ac1a", 213 | "metadata": {}, 214 | "source": [ 215 | "Here’s the Coleman-Reffett operator using EGM.\n", 216 | "\n", 217 | "The key JAX feature here is `vmap`, which vectorizes the computation over the grid points." 218 | ] 219 | }, 220 | { 221 | "cell_type": "code", 222 | "execution_count": null, 223 | "id": "91cedbe0", 224 | "metadata": { 225 | "hide-output": false 226 | }, 227 | "outputs": [], 228 | "source": [ 229 | "def K(\n", 230 | " c_in: jnp.ndarray, # Consumption values on the endogenous grid\n", 231 | " x_in: jnp.ndarray, # Current endogenous grid\n", 232 | " model: Model # Model specification\n", 233 | " ):\n", 234 | " \"\"\"\n", 235 | " The Coleman-Reffett operator using EGM\n", 236 | "\n", 237 | " \"\"\"\n", 238 | " β, μ, s, s_grid, shocks, α = model\n", 239 | " σ = lambda x_val: jnp.interp(x_val, x_in, c_in)\n", 240 | "\n", 241 | " # Define function to compute consumption at a single grid point\n", 242 | " def compute_c(s):\n", 243 | " # Approximate marginal utility ∫ u'(σ(f(s, α)z)) f'(s, α) z ϕ(z)dz\n", 244 | " vals = u_prime(σ(f(s, α) * shocks)) * f_prime(s, α) * shocks\n", 245 | " mu = jnp.mean(vals)\n", 246 | " # Calculate consumption\n", 247 | " return u_prime_inv(β * mu)\n", 248 | "\n", 249 | " # Vectorize and calculate on all exogenous grid points\n", 250 | " compute_c_vectorized = jax.vmap(compute_c)\n", 251 | " c_out = compute_c_vectorized(s_grid)\n", 252 | "\n", 253 | " # Determine corresponding endogenous grid\n", 254 | " x_out = s_grid + c_out # x_i = s_i + c_i\n", 255 | "\n", 256 | " return c_out, x_out" 257 | ] 258 | }, 259 | { 260 | "cell_type": "markdown", 261 | "id": "e8852d94", 262 | "metadata": {}, 263 | "source": [ 264 | "Now we create a model instance." 265 | ] 266 | }, 267 | { 268 | "cell_type": "code", 269 | "execution_count": null, 270 | "id": "46ca8824", 271 | "metadata": { 272 | "hide-output": false 273 | }, 274 | "outputs": [], 275 | "source": [ 276 | "model = create_model()\n", 277 | "s_grid = model.s_grid" 278 | ] 279 | }, 280 | { 281 | "cell_type": "markdown", 282 | "id": "a4fc8c2d", 283 | "metadata": {}, 284 | "source": [ 285 | "The solver uses JAX’s `jax.lax.while_loop` for the iteration and is JIT-compiled for speed." 286 | ] 287 | }, 288 | { 289 | "cell_type": "code", 290 | "execution_count": null, 291 | "id": "65d038be", 292 | "metadata": { 293 | "hide-output": false 294 | }, 295 | "outputs": [], 296 | "source": [ 297 | "@jax.jit\n", 298 | "def solve_model_time_iter(\n", 299 | " model: Model,\n", 300 | " c_init: jnp.ndarray,\n", 301 | " x_init: jnp.ndarray,\n", 302 | " tol: float = 1e-5,\n", 303 | " max_iter: int = 1000\n", 304 | " ):\n", 305 | " \"\"\"\n", 306 | " Solve the model using time iteration with EGM.\n", 307 | " \"\"\"\n", 308 | "\n", 309 | " def condition(loop_state):\n", 310 | " i, c, x, error = loop_state\n", 311 | " return (error > tol) & (i < max_iter)\n", 312 | "\n", 313 | " def body(loop_state):\n", 314 | " i, c, x, error = loop_state\n", 315 | " c_new, x_new = K(c, x, model)\n", 316 | " error = jnp.max(jnp.abs(c_new - c))\n", 317 | " return i + 1, c_new, x_new, error\n", 318 | "\n", 319 | " # Initialize loop state\n", 320 | " initial_state = (0, c_init, x_init, tol + 1)\n", 321 | "\n", 322 | " # Run the loop\n", 323 | " i, c, x, error = jax.lax.while_loop(condition, body, initial_state)\n", 324 | "\n", 325 | " return c, x" 326 | ] 327 | }, 328 | { 329 | "cell_type": "markdown", 330 | "id": "1173e31c", 331 | "metadata": {}, 332 | "source": [ 333 | "We solve the model starting from an initial guess." 334 | ] 335 | }, 336 | { 337 | "cell_type": "code", 338 | "execution_count": null, 339 | "id": "954092f3", 340 | "metadata": { 341 | "hide-output": false 342 | }, 343 | "outputs": [], 344 | "source": [ 345 | "c_init = jnp.copy(s_grid)\n", 346 | "x_init = s_grid + c_init\n", 347 | "c, x = solve_model_time_iter(model, c_init, x_init)" 348 | ] 349 | }, 350 | { 351 | "cell_type": "markdown", 352 | "id": "5b891aa5", 353 | "metadata": {}, 354 | "source": [ 355 | "Let’s plot the resulting policy against the analytical solution." 356 | ] 357 | }, 358 | { 359 | "cell_type": "code", 360 | "execution_count": null, 361 | "id": "c6a370ec", 362 | "metadata": { 363 | "hide-output": false 364 | }, 365 | "outputs": [], 366 | "source": [ 367 | "fig, ax = plt.subplots()\n", 368 | "\n", 369 | "ax.plot(x, c, lw=2,\n", 370 | " alpha=0.8, label='approximate policy function')\n", 371 | "\n", 372 | "ax.plot(x, σ_star(x, model.α, model.β), 'k--',\n", 373 | " lw=2, alpha=0.8, label='true policy function')\n", 374 | "\n", 375 | "ax.legend()\n", 376 | "plt.show()" 377 | ] 378 | }, 379 | { 380 | "cell_type": "markdown", 381 | "id": "cb6c7c65", 382 | "metadata": {}, 383 | "source": [ 384 | "The fit is very good." 385 | ] 386 | }, 387 | { 388 | "cell_type": "code", 389 | "execution_count": null, 390 | "id": "2272a9b2", 391 | "metadata": { 392 | "hide-output": false 393 | }, 394 | "outputs": [], 395 | "source": [ 396 | "max_dev = jnp.max(jnp.abs(c - σ_star(x, model.α, model.β)))\n", 397 | "print(f\"Maximum absolute deviation: {max_dev:.7}\")" 398 | ] 399 | }, 400 | { 401 | "cell_type": "markdown", 402 | "id": "d733991f", 403 | "metadata": {}, 404 | "source": [ 405 | "The JAX implementation is very fast thanks to JIT compilation and vectorization." 406 | ] 407 | }, 408 | { 409 | "cell_type": "code", 410 | "execution_count": null, 411 | "id": "5a7e4995", 412 | "metadata": { 413 | "hide-output": false 414 | }, 415 | "outputs": [], 416 | "source": [ 417 | "with qe.Timer(precision=8):\n", 418 | " c, x = solve_model_time_iter(model, c_init, x_init)\n", 419 | " jax.block_until_ready(c)" 420 | ] 421 | }, 422 | { 423 | "cell_type": "markdown", 424 | "id": "af0550b9", 425 | "metadata": {}, 426 | "source": [ 427 | "This speed comes from:\n", 428 | "\n", 429 | "- JIT compilation of the entire solver \n", 430 | "- Vectorization via `vmap` in the Coleman-Reffett operator \n", 431 | "- Use of `jax.lax.while_loop` instead of a Python loop \n", 432 | "- Efficient JAX array operations throughout " 433 | ] 434 | }, 435 | { 436 | "cell_type": "markdown", 437 | "id": "cd9ba9ae", 438 | "metadata": {}, 439 | "source": [ 440 | "## Exercises" 441 | ] 442 | }, 443 | { 444 | "cell_type": "markdown", 445 | "id": "93b14b9c", 446 | "metadata": {}, 447 | "source": [ 448 | "## Exercise 56.1\n", 449 | "\n", 450 | "Solve the optimal savings problem with CRRA utility\n", 451 | "\n", 452 | "$$\n", 453 | "u(c) = \\frac{c^{1 - \\gamma} - 1}{1 - \\gamma}\n", 454 | "$$\n", 455 | "\n", 456 | "Compare the optimal policies for values of $ \\gamma $ approaching 1 from above (e.g., 1.05, 1.1, 1.2).\n", 457 | "\n", 458 | "Show that as $ \\gamma \\to 1 $, the optimal policy converges to the policy obtained with log utility ($ \\gamma = 1 $).\n", 459 | "\n", 460 | "Hint: Use values of $ \\gamma $ close to 1 to ensure the endogenous grids have similar coverage and make visual comparison easier." 461 | ] 462 | }, 463 | { 464 | "cell_type": "markdown", 465 | "id": "8167e530", 466 | "metadata": {}, 467 | "source": [ 468 | "## Solution\n", 469 | "\n", 470 | "We need to create a version of the Coleman-Reffett operator and solver that work with CRRA utility.\n", 471 | "\n", 472 | "The key is to parameterize the utility functions by $ \\gamma $." 473 | ] 474 | }, 475 | { 476 | "cell_type": "code", 477 | "execution_count": null, 478 | "id": "64e65ff2", 479 | "metadata": { 480 | "hide-output": false 481 | }, 482 | "outputs": [], 483 | "source": [ 484 | "def u_crra(c, γ):\n", 485 | " return (c**(1 - γ) - 1) / (1 - γ)\n", 486 | "\n", 487 | "def u_prime_crra(c, γ):\n", 488 | " return c**(-γ)\n", 489 | "\n", 490 | "def u_prime_inv_crra(x, γ):\n", 491 | " return x**(-1/γ)" 492 | ] 493 | }, 494 | { 495 | "cell_type": "markdown", 496 | "id": "a45b41ca", 497 | "metadata": {}, 498 | "source": [ 499 | "Now we create a version of the Coleman-Reffett operator that takes $ \\gamma $ as a parameter." 500 | ] 501 | }, 502 | { 503 | "cell_type": "code", 504 | "execution_count": null, 505 | "id": "3d03abc0", 506 | "metadata": { 507 | "hide-output": false 508 | }, 509 | "outputs": [], 510 | "source": [ 511 | "def K_crra(\n", 512 | " c_in: jnp.ndarray, # Consumption values on the endogenous grid\n", 513 | " x_in: jnp.ndarray, # Current endogenous grid\n", 514 | " model: Model, # Model specification\n", 515 | " γ: float # CRRA parameter\n", 516 | " ):\n", 517 | " \"\"\"\n", 518 | " The Coleman-Reffett operator using EGM with CRRA utility\n", 519 | " \"\"\"\n", 520 | " # Simplify names\n", 521 | " β, α = model.β, model.α\n", 522 | " s_grid, shocks = model.s_grid, model.shocks\n", 523 | "\n", 524 | " # Linear interpolation of policy using endogenous grid\n", 525 | " σ = lambda x_val: jnp.interp(x_val, x_in, c_in)\n", 526 | "\n", 527 | " # Define function to compute consumption at a single grid point\n", 528 | " def compute_c(s):\n", 529 | " vals = u_prime_crra(σ(f(s, α) * shocks), γ) * f_prime(s, α) * shocks\n", 530 | " return u_prime_inv_crra(β * jnp.mean(vals), γ)\n", 531 | "\n", 532 | " # Vectorize over grid using vmap\n", 533 | " compute_c_vectorized = jax.vmap(compute_c)\n", 534 | " c_out = compute_c_vectorized(s_grid)\n", 535 | "\n", 536 | " # Determine corresponding endogenous grid\n", 537 | " x_out = s_grid + c_out # x_i = s_i + c_i\n", 538 | "\n", 539 | " return c_out, x_out" 540 | ] 541 | }, 542 | { 543 | "cell_type": "markdown", 544 | "id": "35edb324", 545 | "metadata": {}, 546 | "source": [ 547 | "We also need a solver that uses this operator." 548 | ] 549 | }, 550 | { 551 | "cell_type": "code", 552 | "execution_count": null, 553 | "id": "a4772d4d", 554 | "metadata": { 555 | "hide-output": false 556 | }, 557 | "outputs": [], 558 | "source": [ 559 | "@jax.jit\n", 560 | "def solve_model_crra(model: Model,\n", 561 | " c_init: jnp.ndarray,\n", 562 | " x_init: jnp.ndarray,\n", 563 | " γ: float,\n", 564 | " tol: float = 1e-5,\n", 565 | " max_iter: int = 1000):\n", 566 | " \"\"\"\n", 567 | " Solve the model using time iteration with EGM and CRRA utility.\n", 568 | " \"\"\"\n", 569 | "\n", 570 | " def condition(loop_state):\n", 571 | " i, c, x, error = loop_state\n", 572 | " return (error > tol) & (i < max_iter)\n", 573 | "\n", 574 | " def body(loop_state):\n", 575 | " i, c, x, error = loop_state\n", 576 | " c_new, x_new = K_crra(c, x, model, γ)\n", 577 | " error = jnp.max(jnp.abs(c_new - c))\n", 578 | " return i + 1, c_new, x_new, error\n", 579 | "\n", 580 | " # Initialize loop state\n", 581 | " initial_state = (0, c_init, x_init, tol + 1)\n", 582 | "\n", 583 | " # Run the loop\n", 584 | " i, c, x, error = jax.lax.while_loop(condition, body, initial_state)\n", 585 | "\n", 586 | " return c, x" 587 | ] 588 | }, 589 | { 590 | "cell_type": "markdown", 591 | "id": "bacfbfe9", 592 | "metadata": {}, 593 | "source": [ 594 | "Now we solve for $ \\gamma = 1 $ (log utility) and values approaching 1 from above." 595 | ] 596 | }, 597 | { 598 | "cell_type": "code", 599 | "execution_count": null, 600 | "id": "74549fda", 601 | "metadata": { 602 | "hide-output": false 603 | }, 604 | "outputs": [], 605 | "source": [ 606 | "γ_values = [1.0, 1.05, 1.1, 1.2]\n", 607 | "policies = {}\n", 608 | "endogenous_grids = {}\n", 609 | "\n", 610 | "model_crra = create_model()\n", 611 | "\n", 612 | "for γ in γ_values:\n", 613 | " c_init = jnp.copy(model_crra.s_grid)\n", 614 | " x_init = model_crra.s_grid + c_init\n", 615 | " c_gamma, x_gamma = solve_model_crra(model_crra, c_init, x_init, γ)\n", 616 | " jax.block_until_ready(c_gamma)\n", 617 | " policies[γ] = c_gamma\n", 618 | " endogenous_grids[γ] = x_gamma\n", 619 | " print(f\"Solved for γ = {γ}\")" 620 | ] 621 | }, 622 | { 623 | "cell_type": "markdown", 624 | "id": "400ddf6f", 625 | "metadata": {}, 626 | "source": [ 627 | "Plot the policies on their endogenous grids." 628 | ] 629 | }, 630 | { 631 | "cell_type": "code", 632 | "execution_count": null, 633 | "id": "1abe54d1", 634 | "metadata": { 635 | "hide-output": false 636 | }, 637 | "outputs": [], 638 | "source": [ 639 | "fig, ax = plt.subplots()\n", 640 | "\n", 641 | "for γ in γ_values:\n", 642 | " x = endogenous_grids[γ]\n", 643 | " if γ == 1.0:\n", 644 | " ax.plot(x, policies[γ], 'k-', linewidth=2,\n", 645 | " label=f'γ = {γ:.2f} (log utility)', alpha=0.8)\n", 646 | " else:\n", 647 | " ax.plot(x, policies[γ], label=f'γ = {γ:.2f}', alpha=0.8)\n", 648 | "\n", 649 | "ax.set_xlabel('State x')\n", 650 | "ax.set_ylabel('Consumption σ(x)')\n", 651 | "ax.legend()\n", 652 | "ax.set_title('Optimal policies: CRRA utility approaching log case')\n", 653 | "plt.show()" 654 | ] 655 | }, 656 | { 657 | "cell_type": "markdown", 658 | "id": "4a47b7ae", 659 | "metadata": {}, 660 | "source": [ 661 | "Note that the plots for $ \\gamma > 1 $ do not cover the entire x-axis range shown.\n", 662 | "\n", 663 | "This is because the endogenous grid $ x = s + \\sigma(s) $ depends on the consumption policy, which varies with $ \\gamma $.\n", 664 | "\n", 665 | "Let’s check the maximum deviation between the log utility case ($ \\gamma = 1.0 $) and values approaching from above." 666 | ] 667 | }, 668 | { 669 | "cell_type": "code", 670 | "execution_count": null, 671 | "id": "630ab927", 672 | "metadata": { 673 | "hide-output": false 674 | }, 675 | "outputs": [], 676 | "source": [ 677 | "for γ in [1.05, 1.1, 1.2]:\n", 678 | " max_diff = jnp.max(jnp.abs(policies[1.0] - policies[γ]))\n", 679 | " print(f\"Max difference between γ=1.0 and γ={γ}: {max_diff:.6}\")" 680 | ] 681 | }, 682 | { 683 | "cell_type": "markdown", 684 | "id": "e16d27c8", 685 | "metadata": {}, 686 | "source": [ 687 | "As expected, the differences decrease as $ \\gamma $ approaches 1 from above, confirming convergence." 688 | ] 689 | } 690 | ], 691 | "metadata": { 692 | "date": 1764675754.3008704, 693 | "filename": "os_egm_jax.md", 694 | "kernelspec": { 695 | "display_name": "Python", 696 | "language": "python3", 697 | "name": "python3" 698 | }, 699 | "title": "Optimal Savings VI: EGM with JAX" 700 | }, 701 | "nbformat": 4, 702 | "nbformat_minor": 5 703 | } -------------------------------------------------------------------------------- /mccall_correlated.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "763ea35c", 6 | "metadata": {}, 7 | "source": [ 8 | "
\n", 9 | " \n", 10 | " \"QuantEcon\"\n", 11 | " \n", 12 | "
" 13 | ] 14 | }, 15 | { 16 | "cell_type": "markdown", 17 | "id": "d7849f88", 18 | "metadata": {}, 19 | "source": [ 20 | "# Job Search V: Correlated Wage Offers" 21 | ] 22 | }, 23 | { 24 | "cell_type": "markdown", 25 | "id": "16b3863a", 26 | "metadata": {}, 27 | "source": [ 28 | "## Contents\n", 29 | "\n", 30 | "- [Job Search V: Correlated Wage Offers](#Job-Search-V:-Correlated-Wage-Offers) \n", 31 | " - [Overview](#Overview) \n", 32 | " - [The model](#The-model) \n", 33 | " - [Implementation](#Implementation) \n", 34 | " - [Unemployment duration](#Unemployment-duration) \n", 35 | " - [Exercises](#Exercises) " 36 | ] 37 | }, 38 | { 39 | "cell_type": "markdown", 40 | "id": "8fcf1ea9", 41 | "metadata": {}, 42 | "source": [ 43 | "In addition to what’s in Anaconda, this lecture will need the following libraries:" 44 | ] 45 | }, 46 | { 47 | "cell_type": "code", 48 | "execution_count": null, 49 | "id": "92e3dcdf", 50 | "metadata": { 51 | "hide-output": false 52 | }, 53 | "outputs": [], 54 | "source": [ 55 | "!pip install quantecon jax" 56 | ] 57 | }, 58 | { 59 | "cell_type": "markdown", 60 | "id": "0e3572b2", 61 | "metadata": {}, 62 | "source": [ 63 | "## Overview\n", 64 | "\n", 65 | "In this lecture we solve a [McCall style job search model](https://python.quantecon.org/mccall_model.html) with persistent and\n", 66 | "transitory components to wages.\n", 67 | "\n", 68 | "In other words, we relax the unrealistic assumption that randomness in wages is independent over time.\n", 69 | "\n", 70 | "At the same time, we will go back to assuming that jobs are permanent and no separation occurs.\n", 71 | "\n", 72 | "This is to keep the model relatively simple as we study the impact of correlation.\n", 73 | "\n", 74 | "We will use the following imports:" 75 | ] 76 | }, 77 | { 78 | "cell_type": "code", 79 | "execution_count": null, 80 | "id": "18609166", 81 | "metadata": { 82 | "hide-output": false 83 | }, 84 | "outputs": [], 85 | "source": [ 86 | "import matplotlib.pyplot as plt\n", 87 | "import jax\n", 88 | "import jax.numpy as jnp\n", 89 | "import jax.random as jr\n", 90 | "import quantecon as qe\n", 91 | "from typing import NamedTuple" 92 | ] 93 | }, 94 | { 95 | "cell_type": "markdown", 96 | "id": "8cdccf84", 97 | "metadata": {}, 98 | "source": [ 99 | "## The model\n", 100 | "\n", 101 | "Wages at each point in time are given by\n", 102 | "\n", 103 | "$$\n", 104 | "w_t = \\exp(z_t) + y_t\n", 105 | "$$\n", 106 | "\n", 107 | "where\n", 108 | "\n", 109 | "$$\n", 110 | "y_t \\sim \\exp(\\mu + s \\zeta_t)\n", 111 | "\\quad \\text{and} \\quad\n", 112 | "z_{t+1} = d + \\rho z_t + \\sigma \\epsilon_{t+1}\n", 113 | "$$\n", 114 | "\n", 115 | "Here $ \\{ \\zeta_t \\} $ and $ \\{ \\epsilon_t \\} $ are both IID and standard normal.\n", 116 | "\n", 117 | "Here $ \\{y_t\\} $ is a transitory component and $ \\{z_t\\} $ is persistent.\n", 118 | "\n", 119 | "As before, the worker can either\n", 120 | "\n", 121 | "1. accept an offer and work permanently at that wage, or \n", 122 | "1. take unemployment compensation $ c $ and wait till next period. \n", 123 | "\n", 124 | "\n", 125 | "The value function satisfies the Bellman equation\n", 126 | "\n", 127 | "$$\n", 128 | "v^*(w, z) =\n", 129 | " \\max\n", 130 | " \\left\\{\n", 131 | " \\frac{u(w)}{1-\\beta}, u(c) + \\beta \\, \\mathbb E_z v^*(w', z')\n", 132 | " \\right\\}\n", 133 | "$$\n", 134 | "\n", 135 | "In this expression, $ u $ is a utility function and $ \\mathbb E_z $ is expectation of next period variables given current $ z $.\n", 136 | "\n", 137 | "The variable $ z $ enters as a state in the Bellman equation because its current value helps predict future wages." 138 | ] 139 | }, 140 | { 141 | "cell_type": "markdown", 142 | "id": "5d210e88", 143 | "metadata": {}, 144 | "source": [ 145 | "### A simplification\n", 146 | "\n", 147 | "There is a way that we can reduce dimensionality in this problem, which greatly accelerates computation.\n", 148 | "\n", 149 | "To start, let $ f^* $ be the continuation value function, defined\n", 150 | "by\n", 151 | "\n", 152 | "$$\n", 153 | "f^*(z) := u(c) + \\beta \\, \\mathbb E_z v^*(w', z')\n", 154 | "$$\n", 155 | "\n", 156 | "The Bellman equation can now be written\n", 157 | "\n", 158 | "$$\n", 159 | "v^*(w, z) = \\max \\left\\{ \\frac{u(w)}{1-\\beta}, \\, f^*(z) \\right\\}\n", 160 | "$$\n", 161 | "\n", 162 | "Combining the last two expressions, we see that the continuation value\n", 163 | "function satisfies\n", 164 | "\n", 165 | "$$\n", 166 | "f^*(z) = u(c) + \\beta \\, \\mathbb E_z \\max \\left\\{ \\frac{u(w')}{1-\\beta}, f^*(z') \\right\\}\n", 167 | "$$\n", 168 | "\n", 169 | "We’ll solve this functional equation for $ f^* $ by introducing the\n", 170 | "operator\n", 171 | "\n", 172 | "$$\n", 173 | "Qf(z) = u(c) + \\beta \\, \\mathbb E_z \\max \\left\\{ \\frac{u(w')}{1-\\beta}, f(z') \\right\\}\n", 174 | "$$\n", 175 | "\n", 176 | "By construction, $ f^* $ is a fixed point of $ Q $, in the sense that\n", 177 | "$ Q f^* = f^* $.\n", 178 | "\n", 179 | "Under mild assumptions, it can be shown that $ Q $ is a [contraction mapping](https://en.wikipedia.org/wiki/Contraction_mapping) over a suitable space of continuous functions on $ \\mathbb R $.\n", 180 | "\n", 181 | "By Banach’s contraction mapping theorem, this means that $ f^* $ is the unique fixed point and we can calculate it by iterating with $ Q $ from any reasonable initial condition.\n", 182 | "\n", 183 | "Once we have $ f^* $, we can solve the search problem by stopping when the reward for accepting exceeds the continuation value, or\n", 184 | "\n", 185 | "$$\n", 186 | "\\frac{u(w)}{1-\\beta} \\geq f^*(z)\n", 187 | "$$\n", 188 | "\n", 189 | "For utility we take $ u(c) = \\ln(c) $.\n", 190 | "\n", 191 | "The reservation wage is the wage where equality holds in the last expression.\n", 192 | "\n", 193 | "That is,\n", 194 | "\n", 195 | "\n", 196 | "\n", 197 | "$$\n", 198 | "\\bar w (z) := \\exp(f^*(z) (1-\\beta)) \\tag{46.1}\n", 199 | "$$\n", 200 | "\n", 201 | "Our main aim is to solve for the reservation rule and study its properties and implications." 202 | ] 203 | }, 204 | { 205 | "cell_type": "markdown", 206 | "id": "9e97a4d5", 207 | "metadata": {}, 208 | "source": [ 209 | "## Implementation\n", 210 | "\n", 211 | "Let $ f $ be our initial guess of $ f^* $.\n", 212 | "\n", 213 | "When we iterate, we use the [fitted value function iteration](https://python.quantecon.org/mccall_fitted_vfi.html) algorithm.\n", 214 | "\n", 215 | "In particular, $ f $ and all subsequent iterates are stored as a vector of values on a grid.\n", 216 | "\n", 217 | "These points are interpolated into a function as required, using piecewise linear interpolation.\n", 218 | "\n", 219 | "The integral in the definition of $ Qf $ is calculated by Monte Carlo.\n", 220 | "\n", 221 | "Here’s a `NamedTuple` that stores the model parameters and data.\n", 222 | "\n", 223 | "Default parameter values are embedded in the model." 224 | ] 225 | }, 226 | { 227 | "cell_type": "code", 228 | "execution_count": null, 229 | "id": "399b8249", 230 | "metadata": { 231 | "hide-output": false 232 | }, 233 | "outputs": [], 234 | "source": [ 235 | "class JobSearchModel(NamedTuple):\n", 236 | " μ: float # transient shock log mean\n", 237 | " s: float # transient shock log variance \n", 238 | " d: float # shift coefficient of persistent state\n", 239 | " ρ: float # correlation coefficient of persistent state\n", 240 | " σ: float # state volatility\n", 241 | " β: float # discount factor\n", 242 | " c: float # unemployment compensation\n", 243 | " z_grid: jnp.ndarray \n", 244 | " e_draws: jnp.ndarray\n", 245 | "\n", 246 | "def create_job_search_model(μ=0.0, s=1.0, d=0.0, ρ=0.9, σ=0.1, β=0.98, c=5.0, \n", 247 | " mc_size=1000, grid_size=100, key=jr.PRNGKey(1234)):\n", 248 | " \"\"\"\n", 249 | " Create a JobSearchModel with computed grid and draws.\n", 250 | " \"\"\"\n", 251 | " # Set up grid\n", 252 | " z_mean = d / (1 - ρ)\n", 253 | " z_sd = σ / jnp.sqrt(1 - ρ**2)\n", 254 | " k = 3 # std devs from mean\n", 255 | " a, b = z_mean - k * z_sd, z_mean + k * z_sd\n", 256 | " z_grid = jnp.linspace(a, b, grid_size)\n", 257 | "\n", 258 | " # Draw and store shocks\n", 259 | " e_draws = jr.normal(key, (2, mc_size))\n", 260 | "\n", 261 | " return JobSearchModel(μ=μ, s=s, d=d, ρ=ρ, σ=σ, β=β, c=c, \n", 262 | " z_grid=z_grid, e_draws=e_draws)" 263 | ] 264 | }, 265 | { 266 | "cell_type": "markdown", 267 | "id": "37bc5561", 268 | "metadata": {}, 269 | "source": [ 270 | "Next we implement the $ Q $ operator." 271 | ] 272 | }, 273 | { 274 | "cell_type": "code", 275 | "execution_count": null, 276 | "id": "4b4cf20d", 277 | "metadata": { 278 | "hide-output": false 279 | }, 280 | "outputs": [], 281 | "source": [ 282 | "def Q(model, f_in):\n", 283 | " \"\"\"\n", 284 | " Apply the operator Q.\n", 285 | "\n", 286 | " * model is an instance of JobSearchModel\n", 287 | " * f_in is an array that represents f\n", 288 | " * returns Qf\n", 289 | "\n", 290 | " \"\"\"\n", 291 | " μ, s, d = model.μ, model.s, model.d\n", 292 | " ρ, σ, β, c = model.ρ, model.σ, model.β, model.c\n", 293 | " z_grid, e_draws = model.z_grid, model.e_draws\n", 294 | " M = e_draws.shape[1]\n", 295 | "\n", 296 | " def compute_expectation(z):\n", 297 | " def evaluate_shock(e):\n", 298 | " e1, e2 = e[0], e[1]\n", 299 | " z_next = d + ρ * z + σ * e1\n", 300 | " go_val = jnp.interp(z_next, z_grid, f_in) # f(z')\n", 301 | " y_next = jnp.exp(μ + s * e2) # y' draw\n", 302 | " w_next = jnp.exp(z_next) + y_next # w' draw\n", 303 | " stop_val = jnp.log(w_next) / (1 - β)\n", 304 | " return jnp.maximum(stop_val, go_val)\n", 305 | " \n", 306 | " expectations = jax.vmap(evaluate_shock)(e_draws.T)\n", 307 | " return jnp.mean(expectations)\n", 308 | "\n", 309 | " expectations = jax.vmap(compute_expectation)(z_grid)\n", 310 | " f_out = jnp.log(c) + β * expectations\n", 311 | " return f_out" 312 | ] 313 | }, 314 | { 315 | "cell_type": "markdown", 316 | "id": "c11d8409", 317 | "metadata": {}, 318 | "source": [ 319 | "Here’s a function to compute an approximation to the fixed point of $ Q $." 320 | ] 321 | }, 322 | { 323 | "cell_type": "code", 324 | "execution_count": null, 325 | "id": "5c8e6340", 326 | "metadata": { 327 | "hide-output": false 328 | }, 329 | "outputs": [], 330 | "source": [ 331 | "@jax.jit \n", 332 | "def compute_fixed_point(model, tol=1e-4, max_iter=1000):\n", 333 | " \"\"\"\n", 334 | " Compute an approximation to the fixed point of Q.\n", 335 | " \"\"\"\n", 336 | " \n", 337 | " def cond_fun(loop_state):\n", 338 | " f, i, error = loop_state\n", 339 | " return jnp.logical_and(error > tol, i < max_iter)\n", 340 | " \n", 341 | " def body_fun(loop_state):\n", 342 | " f, i, error = loop_state\n", 343 | " f_new = Q(model, f)\n", 344 | " error_new = jnp.max(jnp.abs(f_new - f))\n", 345 | " return f_new, i + 1, error_new\n", 346 | " \n", 347 | " # Initial state\n", 348 | " f_init = jnp.full(len(model.z_grid), jnp.log(model.c))\n", 349 | " init_state = (f_init, 0, tol + 1)\n", 350 | " \n", 351 | " # Run iteration\n", 352 | " f_final, iterations, final_error = jax.lax.while_loop(\n", 353 | " cond_fun, body_fun, init_state\n", 354 | " )\n", 355 | " \n", 356 | " return f_final" 357 | ] 358 | }, 359 | { 360 | "cell_type": "markdown", 361 | "id": "8d282aa0", 362 | "metadata": {}, 363 | "source": [ 364 | "Let’s try generating an instance and solving the model." 365 | ] 366 | }, 367 | { 368 | "cell_type": "code", 369 | "execution_count": null, 370 | "id": "3eda10f6", 371 | "metadata": { 372 | "hide-output": false 373 | }, 374 | "outputs": [], 375 | "source": [ 376 | "model = create_job_search_model()\n", 377 | "\n", 378 | "with qe.Timer():\n", 379 | " f_star = compute_fixed_point(model).block_until_ready()" 380 | ] 381 | }, 382 | { 383 | "cell_type": "markdown", 384 | "id": "394335fa", 385 | "metadata": {}, 386 | "source": [ 387 | "Next we will compute and plot the reservation wage function defined in [(46.1)](#equation-corr-mcm-barw)." 388 | ] 389 | }, 390 | { 391 | "cell_type": "code", 392 | "execution_count": null, 393 | "id": "2f2b05c8", 394 | "metadata": { 395 | "hide-output": false 396 | }, 397 | "outputs": [], 398 | "source": [ 399 | "res_wage_function = jnp.exp(f_star * (1 - model.β))\n", 400 | "\n", 401 | "fig, ax = plt.subplots()\n", 402 | "ax.plot(\n", 403 | " model.z_grid, res_wage_function, label=\"reservation wage given $z$\"\n", 404 | ")\n", 405 | "ax.set(xlabel=\"$z$\", ylabel=\"wage\")\n", 406 | "ax.legend()\n", 407 | "plt.show()" 408 | ] 409 | }, 410 | { 411 | "cell_type": "markdown", 412 | "id": "28284895", 413 | "metadata": {}, 414 | "source": [ 415 | "Notice that the reservation wage is increasing in the current state $ z $.\n", 416 | "\n", 417 | "This is because a higher state leads the agent to predict higher future wages,\n", 418 | "increasing the option value of waiting.\n", 419 | "\n", 420 | "Let’s try changing unemployment compensation and look at its impact on the\n", 421 | "reservation wage:" 422 | ] 423 | }, 424 | { 425 | "cell_type": "code", 426 | "execution_count": null, 427 | "id": "5d55d336", 428 | "metadata": { 429 | "hide-output": false 430 | }, 431 | "outputs": [], 432 | "source": [ 433 | "c_vals = 1, 2, 3\n", 434 | "\n", 435 | "fig, ax = plt.subplots()\n", 436 | "\n", 437 | "for c in c_vals:\n", 438 | " model = create_job_search_model(c=c)\n", 439 | " f_star = compute_fixed_point(model)\n", 440 | " res_wage_function = jnp.exp(f_star * (1 - model.β))\n", 441 | " ax.plot(model.z_grid, res_wage_function, \n", 442 | " label=rf\"$\\bar w$ at $c = {c}$\")\n", 443 | "\n", 444 | "ax.set(xlabel=\"$z$\", ylabel=\"wage\")\n", 445 | "ax.legend()\n", 446 | "plt.show()" 447 | ] 448 | }, 449 | { 450 | "cell_type": "markdown", 451 | "id": "947a6849", 452 | "metadata": {}, 453 | "source": [ 454 | "As expected, higher unemployment compensation shifts the reservation wage up\n", 455 | "at all state values." 456 | ] 457 | }, 458 | { 459 | "cell_type": "markdown", 460 | "id": "4c76203e", 461 | "metadata": {}, 462 | "source": [ 463 | "## Unemployment duration\n", 464 | "\n", 465 | "Next we study how mean unemployment duration varies with unemployment compensation.\n", 466 | "\n", 467 | "For simplicity we’ll fix the initial state at $ z_t = 0 $." 468 | ] 469 | }, 470 | { 471 | "cell_type": "code", 472 | "execution_count": null, 473 | "id": "5728e50b", 474 | "metadata": { 475 | "hide-output": false 476 | }, 477 | "outputs": [], 478 | "source": [ 479 | "def compute_unemployment_duration(\n", 480 | " model, key=jr.PRNGKey(1234), num_reps=100_000\n", 481 | " ):\n", 482 | " \"\"\"\n", 483 | " Compute expected unemployment duration.\n", 484 | "\n", 485 | " \"\"\"\n", 486 | " f_star = compute_fixed_point(model)\n", 487 | " μ, s, d = model.μ, model.s, model.d\n", 488 | " ρ, σ, β, c = model.ρ, model.σ, model.β, model.c\n", 489 | " z_grid = model.z_grid\n", 490 | "\n", 491 | " @jax.jit\n", 492 | " def f_star_function(z):\n", 493 | " return jnp.interp(z, z_grid, f_star)\n", 494 | "\n", 495 | " @jax.jit\n", 496 | " def draw_τ(key, t_max=10_000):\n", 497 | " def cond_fun(loop_state):\n", 498 | " z, t, unemployed, key = loop_state\n", 499 | " return jnp.logical_and(unemployed, t < t_max)\n", 500 | " \n", 501 | " def body_fun(loop_state):\n", 502 | " z, t, unemployed, key = loop_state\n", 503 | " key1, key2, key = jr.split(key, 3)\n", 504 | " \n", 505 | " # Draw current wage\n", 506 | " y = jnp.exp(μ + s * jr.normal(key1))\n", 507 | " w = jnp.exp(z) + y\n", 508 | " res_wage = jnp.exp(f_star_function(z) * (1 - β))\n", 509 | " \n", 510 | " # Check if optimal to stop\n", 511 | " accept = w >= res_wage\n", 512 | " τ = jnp.where(accept, t, t_max)\n", 513 | " \n", 514 | " # Update state if not accepting\n", 515 | " z_new = jnp.where(accept, z, \n", 516 | " ρ * z + d + σ * jr.normal(key2))\n", 517 | " t_new = t + 1\n", 518 | " unemployed_new = jnp.logical_not(accept)\n", 519 | " \n", 520 | " return z_new, t_new, unemployed_new, key\n", 521 | " \n", 522 | " # Initial loop_state: (z, t, unemployed, key)\n", 523 | " init_state = (0.0, 0, True, key)\n", 524 | " z_final, t_final, unemployed_final, _ = jax.lax.while_loop(\n", 525 | " cond_fun, body_fun, init_state)\n", 526 | " \n", 527 | " # Return final time if job found, otherwise t_max\n", 528 | " return jnp.where(unemployed_final, t_max, t_final)\n", 529 | "\n", 530 | " # Generate keys for all simulations\n", 531 | " keys = jr.split(key, num_reps)\n", 532 | " \n", 533 | " # Vectorize over simulations\n", 534 | " τ_vals = jax.vmap(draw_τ)(keys)\n", 535 | " \n", 536 | " return jnp.mean(τ_vals)" 537 | ] 538 | }, 539 | { 540 | "cell_type": "markdown", 541 | "id": "be1b0865", 542 | "metadata": {}, 543 | "source": [ 544 | "Let’s test this out with some possible values for unemployment compensation." 545 | ] 546 | }, 547 | { 548 | "cell_type": "code", 549 | "execution_count": null, 550 | "id": "c9c1449d", 551 | "metadata": { 552 | "hide-output": false 553 | }, 554 | "outputs": [], 555 | "source": [ 556 | "c_vals = jnp.linspace(1.0, 10.0, 8)\n", 557 | "durations = []\n", 558 | "for i, c in enumerate(c_vals):\n", 559 | " model = create_job_search_model(c=c)\n", 560 | " τ = compute_unemployment_duration(model, num_reps=10_000)\n", 561 | " durations.append(τ)\n", 562 | "durations = jnp.array(durations)" 563 | ] 564 | }, 565 | { 566 | "cell_type": "markdown", 567 | "id": "761c6a82", 568 | "metadata": {}, 569 | "source": [ 570 | "Here is a plot of the results." 571 | ] 572 | }, 573 | { 574 | "cell_type": "code", 575 | "execution_count": null, 576 | "id": "6c0f7602", 577 | "metadata": { 578 | "hide-output": false 579 | }, 580 | "outputs": [], 581 | "source": [ 582 | "fig, ax = plt.subplots()\n", 583 | "ax.plot(c_vals, durations)\n", 584 | "ax.set_xlabel(\"unemployment compensation\")\n", 585 | "ax.set_ylabel(\"mean unemployment duration\")\n", 586 | "plt.show()" 587 | ] 588 | }, 589 | { 590 | "cell_type": "markdown", 591 | "id": "f8b0bffc", 592 | "metadata": {}, 593 | "source": [ 594 | "Not surprisingly, unemployment duration increases when unemployment compensation is higher.\n", 595 | "\n", 596 | "This is because the value of waiting increases with unemployment compensation." 597 | ] 598 | }, 599 | { 600 | "cell_type": "markdown", 601 | "id": "4b0d41f7", 602 | "metadata": {}, 603 | "source": [ 604 | "## Exercises" 605 | ] 606 | }, 607 | { 608 | "cell_type": "markdown", 609 | "id": "287c75a0", 610 | "metadata": {}, 611 | "source": [ 612 | "## Exercise 46.1\n", 613 | "\n", 614 | "Investigate how mean unemployment duration varies with the discount factor $ \\beta $.\n", 615 | "\n", 616 | "- What is your prior expectation? \n", 617 | "- Do your results match up? " 618 | ] 619 | }, 620 | { 621 | "cell_type": "markdown", 622 | "id": "7a1d64c3", 623 | "metadata": {}, 624 | "source": [ 625 | "## Solution\n", 626 | "\n", 627 | "Here is one solution" 628 | ] 629 | }, 630 | { 631 | "cell_type": "code", 632 | "execution_count": null, 633 | "id": "60572711", 634 | "metadata": { 635 | "hide-output": false 636 | }, 637 | "outputs": [], 638 | "source": [ 639 | "beta_vals = jnp.linspace(0.94, 0.99, 8)\n", 640 | "durations = []\n", 641 | "for i, β in enumerate(beta_vals):\n", 642 | " model = create_job_search_model(β=β)\n", 643 | " τ = compute_unemployment_duration(model, num_reps=10_000)\n", 644 | " durations.append(τ)\n", 645 | "durations = jnp.array(durations)" 646 | ] 647 | }, 648 | { 649 | "cell_type": "code", 650 | "execution_count": null, 651 | "id": "fc905921", 652 | "metadata": { 653 | "hide-output": false 654 | }, 655 | "outputs": [], 656 | "source": [ 657 | "fig, ax = plt.subplots()\n", 658 | "ax.plot(beta_vals, durations)\n", 659 | "ax.set_xlabel(r\"$\\beta$\")\n", 660 | "ax.set_ylabel(\"mean unemployment duration\")\n", 661 | "plt.show()" 662 | ] 663 | }, 664 | { 665 | "cell_type": "markdown", 666 | "id": "0c35262b", 667 | "metadata": {}, 668 | "source": [ 669 | "The figure shows that more patient individuals tend to wait longer before accepting an offer." 670 | ] 671 | } 672 | ], 673 | "metadata": { 674 | "date": 1762637654.2854698, 675 | "filename": "mccall_correlated.md", 676 | "kernelspec": { 677 | "display_name": "Python", 678 | "language": "python3", 679 | "name": "python3" 680 | }, 681 | "title": "Job Search V: Correlated Wage Offers" 682 | }, 683 | "nbformat": 4, 684 | "nbformat_minor": 5 685 | } --------------------------------------------------------------------------------