{ "cells": [ { "cell_type": "markdown", "id": "8835416e", "metadata": {}, "source": [ "\n", "" ] }, { "cell_type": "markdown", "id": "69452a11", "metadata": {}, "source": [ "# Job Search V: Modeling Career Choice\n", "\n", "\n", "" ] }, { "cell_type": "markdown", "id": "1d4ee6f3", "metadata": {}, "source": [ "## Contents\n", "\n", "- [Job Search V: Modeling Career Choice](#Job-Search-V:-Modeling-Career-Choice) \n", " - [Overview](#Overview) \n", " - [Model](#Model) \n", " - [Implementation](#Implementation) \n", " - [Exercises](#Exercises) " ] }, { "cell_type": "markdown", "id": "194485be", "metadata": {}, "source": [ "In addition to what’s in Anaconda, this lecture will need the following libraries:" ] }, { "cell_type": "code", "execution_count": null, "id": "8821318f", "metadata": { "hide-output": false }, "outputs": [], "source": [ "!pip install quantecon" ] }, { "cell_type": "markdown", "id": "7524cb5e", "metadata": {}, "source": [ "## Overview\n", "\n", "Next, we study a computational problem concerning career and job choices.\n", "\n", "The model is originally due to Derek Neal [[Neal, 1999](https://python.quantecon.org/zreferences.html#id199)].\n", "\n", "This exposition draws on the presentation in [[Ljungqvist and Sargent, 2018](https://python.quantecon.org/zreferences.html#id185)], section 6.5.\n", "\n", "We begin with some imports:" ] }, { "cell_type": "code", "execution_count": null, "id": "c46a984f", "metadata": { "hide-output": false }, "outputs": [], "source": [ "import matplotlib.pyplot as plt\n", "plt.rcParams[\"figure.figsize\"] = (11, 5) #set default figure size\n", "import numpy as np\n", "import quantecon as qe\n", "from numba import njit, prange\n", "from quantecon.distributions import BetaBinomial\n", "from scipy.special import binom, beta\n", "from mpl_toolkits.mplot3d.axes3d import Axes3D\n", "from matplotlib import cm" ] }, { "cell_type": "markdown", "id": "8a38f12a", "metadata": {}, "source": [ "### Model Features\n", "\n", "- Career and job within career both chosen to maximize expected discounted wage flow. \n", "- Infinite horizon dynamic programming with two state variables. " ] }, { "cell_type": "markdown", "id": "9666afc5", "metadata": {}, "source": [ "## Model\n", "\n", "In what follows we distinguish between a career and a job, where\n", "\n", "- a *career* is understood to be a general field encompassing many possible jobs, and \n", "- a *job* is understood to be a position with a particular firm \n", "\n", "\n", "For workers, wages can be decomposed into the contribution of job and career\n", "\n", "- $ w_t = \\theta_t + \\epsilon_t $, where \n", " - $ \\theta_t $ is the contribution of career at time $ t $ \n", " - $ \\epsilon_t $ is the contribution of the job at time $ t $ \n", "\n", "\n", "At the start of time $ t $, a worker has the following options\n", "\n", "- retain a current (career, job) pair $ (\\theta_t, \\epsilon_t) $\n", " — referred to hereafter as “stay put” \n", "- retain a current career $ \\theta_t $ but redraw a job $ \\epsilon_t $\n", " — referred to hereafter as “new job” \n", "- redraw both a career $ \\theta_t $ and a job $ \\epsilon_t $\n", " — referred to hereafter as “new life” \n", "\n", "\n", "Draws of $ \\theta $ and $ \\epsilon $ are independent of each other and\n", "past values, with\n", "\n", "- $ \\theta_t \\sim F $ \n", "- $ \\epsilon_t \\sim G $ \n", "\n", "\n", "Notice that the worker does not have the option to retain a job but redraw\n", "a career — starting a new career always requires starting a new job.\n", "\n", "A young worker aims to maximize the expected sum of discounted wages\n", "\n", "\n", "\n", "$$\n", "\\mathbb{E} \\sum_{t=0}^{\\infty} \\beta^t w_t \\tag{31.1}\n", "$$\n", "\n", "subject to the choice restrictions specified above.\n", "\n", "Let $ v(\\theta, \\epsilon) $ denote the value function, which is the\n", "maximum of [(31.1)](#equation-exw) overall feasible (career, job) policies, given the\n", "initial state $ (\\theta, \\epsilon) $.\n", "\n", "The value function obeys\n", "\n", "$$\n", "v(\\theta, \\epsilon) = \\max\\{I, II, III\\}\n", "$$\n", "\n", "where\n", "\n", "\n", "\n", "$$\n", "\\begin{aligned}\n", "& I = \\theta + \\epsilon + \\beta v(\\theta, \\epsilon) \\\\\n", "& II = \\theta + \\int \\epsilon' G(d \\epsilon') + \\beta \\int v(\\theta, \\epsilon') G(d \\epsilon') \\nonumber \\\\\n", "& III = \\int \\theta' F(d \\theta') + \\int \\epsilon' G(d \\epsilon') + \\beta \\int \\int v(\\theta', \\epsilon') G(d \\epsilon') F(d \\theta') \\nonumber\n", "\\end{aligned} \\tag{31.2}\n", "$$\n", "\n", "Evidently $ I $, $ II $ and $ III $ correspond to “stay put”, “new job” and “new life”, respectively." ] }, { "cell_type": "markdown", "id": "9af9a814", "metadata": {}, "source": [ "### Parameterization\n", "\n", "As in [[Ljungqvist and Sargent, 2018](https://python.quantecon.org/zreferences.html#id185)], section 6.5, we will focus on a discrete version of the model, parameterized as follows:\n", "\n", "- both $ \\theta $ and $ \\epsilon $ take values in the set\n", " `np.linspace(0, B, grid_size)` — an even grid of points between\n", " $ 0 $ and $ B $ inclusive \n", "- `grid_size = 50` \n", "- `B = 5` \n", "- `β = 0.95` \n", "\n", "\n", "The distributions $ F $ and $ G $ are discrete distributions\n", "generating draws from the grid points `np.linspace(0, B, grid_size)`.\n", "\n", "A very useful family of discrete distributions is the Beta-binomial family,\n", "with probability mass function\n", "\n", "$$\n", "p(k \\,|\\, n, a, b)\n", "= {n \\choose k} \\frac{B(k + a, n - k + b)}{B(a, b)},\n", "\\qquad k = 0, \\ldots, n\n", "$$\n", "\n", "Interpretation:\n", "\n", "- draw $ q $ from a Beta distribution with shape parameters $ (a, b) $ \n", "- run $ n $ independent binary trials, each with success probability $ q $ \n", "- $ p(k \\,|\\, n, a, b) $ is the probability of $ k $ successes in these $ n $ trials \n", "\n", "\n", "Nice properties:\n", "\n", "- very flexible class of distributions, including uniform, symmetric unimodal, etc. \n", "- only three parameters \n", "\n", "\n", "Here’s a figure showing the effect on the pmf of different shape parameters when $ n=50 $." ] }, { "cell_type": "code", "execution_count": null, "id": "0b91121f", "metadata": { "hide-output": false }, "outputs": [], "source": [ "def gen_probs(n, a, b):\n", " probs = np.zeros(n+1)\n", " for k in range(n+1):\n", " probs[k] = binom(n, k) * beta(k + a, n - k + b) / beta(a, b)\n", " return probs\n", "\n", "n = 50\n", "a_vals = [0.5, 1, 100]\n", "b_vals = [0.5, 1, 100]\n", "fig, ax = plt.subplots(figsize=(10, 6))\n", "for a, b in zip(a_vals, b_vals):\n", " ab_label = f'$a = {a:.1f}$, $b = {b:.1f}$'\n", " ax.plot(list(range(0, n+1)), gen_probs(n, a, b), '-o', label=ab_label)\n", "ax.legend()\n", "plt.show()" ] }, { "cell_type": "markdown", "id": "5cb0b5fc", "metadata": {}, "source": [ "## Implementation\n", "\n", "We will first create a class `CareerWorkerProblem` which will hold the\n", "default parameterizations of the model and an initial guess for the value function." ] }, { "cell_type": "code", "execution_count": null, "id": "db553c57", "metadata": { "hide-output": false }, "outputs": [], "source": [ "class CareerWorkerProblem:\n", "\n", " def __init__(self,\n", " B=5.0, # Upper bound\n", " β=0.95, # Discount factor\n", " grid_size=50, # Grid size\n", " F_a=1,\n", " F_b=1,\n", " G_a=1,\n", " G_b=1):\n", "\n", " self.β, self.grid_size, self.B = β, grid_size, B\n", "\n", " self.θ = np.linspace(0, B, grid_size) # Set of θ values\n", " self.ϵ = np.linspace(0, B, grid_size) # Set of ϵ values\n", "\n", " self.F_probs = BetaBinomial(grid_size - 1, F_a, F_b).pdf()\n", " self.G_probs = BetaBinomial(grid_size - 1, G_a, G_b).pdf()\n", " self.F_mean = np.sum(self.θ * self.F_probs)\n", " self.G_mean = np.sum(self.ϵ * self.G_probs)\n", "\n", " # Store these parameters for str and repr methods\n", " self._F_a, self._F_b = F_a, F_b\n", " self._G_a, self._G_b = G_a, G_b" ] }, { "cell_type": "markdown", "id": "8e7bf087", "metadata": {}, "source": [ "The following function takes an instance of `CareerWorkerProblem` and returns\n", "the corresponding Bellman operator $ T $ and the greedy policy function.\n", "\n", "In this model, $ T $ is defined by $ Tv(\\theta, \\epsilon) = \\max\\{I, II, III\\} $, where\n", "$ I $, $ II $ and $ III $ are as given in [(31.2)](#equation-eyes)." ] }, { "cell_type": "code", "execution_count": null, "id": "fda2d9a1", "metadata": { "hide-output": false }, "outputs": [], "source": [ "def operator_factory(cw, parallel_flag=True):\n", "\n", " \"\"\"\n", " Returns jitted versions of the Bellman operator and the\n", " greedy policy function\n", "\n", " cw is an instance of ``CareerWorkerProblem``\n", " \"\"\"\n", "\n", " θ, ϵ, β = cw.θ, cw.ϵ, cw.β\n", " F_probs, G_probs = cw.F_probs, cw.G_probs\n", " F_mean, G_mean = cw.F_mean, cw.G_mean\n", "\n", " @njit(parallel=parallel_flag)\n", " def T(v):\n", " \"The Bellman operator\"\n", "\n", " v_new = np.empty_like(v)\n", "\n", " for i in prange(len(v)):\n", " for j in prange(len(v)):\n", " v1 = θ[i] + ϵ[j] + β * v[i, j] # Stay put\n", " v2 = θ[i] + G_mean + β * v[i, :] @ G_probs # New job\n", " v3 = G_mean + F_mean + β * F_probs @ v @ G_probs # New life\n", " v_new[i, j] = max(v1, v2, v3)\n", "\n", " return v_new\n", "\n", " @njit\n", " def get_greedy(v):\n", " \"Computes the v-greedy policy\"\n", "\n", " σ = np.empty(v.shape)\n", "\n", " for i in range(len(v)):\n", " for j in range(len(v)):\n", " v1 = θ[i] + ϵ[j] + β * v[i, j]\n", " v2 = θ[i] + G_mean + β * v[i, :] @ G_probs\n", " v3 = G_mean + F_mean + β * F_probs @ v @ G_probs\n", " if v1 > max(v2, v3):\n", " action = 1\n", " elif v2 > max(v1, v3):\n", " action = 2\n", " else:\n", " action = 3\n", " σ[i, j] = action\n", "\n", " return σ\n", "\n", " return T, get_greedy" ] }, { "cell_type": "markdown", "id": "0f671ade", "metadata": {}, "source": [ "Lastly, `solve_model` will take an instance of `CareerWorkerProblem` and\n", "iterate using the Bellman operator to find the fixed point of the Bellman equation." ] }, { "cell_type": "code", "execution_count": null, "id": "892f1089", "metadata": { "hide-output": false }, "outputs": [], "source": [ "def solve_model(cw,\n", " use_parallel=True,\n", " tol=1e-4,\n", " max_iter=1000,\n", " verbose=True,\n", " print_skip=25):\n", "\n", " T, _ = operator_factory(cw, parallel_flag=use_parallel)\n", "\n", " # Set up loop\n", " v = np.full((cw.grid_size, cw.grid_size), 100.) # Initial guess\n", " i = 0\n", " error = tol + 1\n", "\n", " while i < max_iter and error > tol:\n", " v_new = T(v)\n", " error = np.max(np.abs(v - v_new))\n", " i += 1\n", " if verbose and i % print_skip == 0:\n", " print(f\"Error at iteration {i} is {error}.\")\n", " v = v_new\n", "\n", " if error > tol:\n", " print(\"Failed to converge!\")\n", "\n", " elif verbose:\n", " print(f\"\\nConverged in {i} iterations.\")\n", "\n", " return v_new" ] }, { "cell_type": "markdown", "id": "627538b7", "metadata": {}, "source": [ "Here’s the solution to the model – an approximate value function" ] }, { "cell_type": "code", "execution_count": null, "id": "e056ebf3", "metadata": { "hide-output": false }, "outputs": [], "source": [ "cw = CareerWorkerProblem()\n", "T, get_greedy = operator_factory(cw)\n", "v_star = solve_model(cw, verbose=False)\n", "greedy_star = get_greedy(v_star)\n", "\n", "fig = plt.figure(figsize=(8, 6))\n", "ax = fig.add_subplot(111, projection='3d')\n", "tg, eg = np.meshgrid(cw.θ, cw.ϵ)\n", "ax.plot_surface(tg,\n", " eg,\n", " v_star.T,\n", " cmap=cm.jet,\n", " alpha=0.5,\n", " linewidth=0.25)\n", "ax.set(xlabel='θ', ylabel='ϵ', zlim=(150, 200))\n", "ax.view_init(ax.elev, 225)\n", "plt.show()" ] }, { "cell_type": "markdown", "id": "871441e9", "metadata": {}, "source": [ "And here is the optimal policy" ] }, { "cell_type": "code", "execution_count": null, "id": "f33c6f42", "metadata": { "hide-output": false }, "outputs": [], "source": [ "fig, ax = plt.subplots(figsize=(6, 6))\n", "tg, eg = np.meshgrid(cw.θ, cw.ϵ)\n", "lvls = (0.5, 1.5, 2.5, 3.5)\n", "ax.contourf(tg, eg, greedy_star.T, levels=lvls, cmap=cm.winter, alpha=0.5)\n", "ax.contour(tg, eg, greedy_star.T, colors='k', levels=lvls, linewidths=2)\n", "ax.set(xlabel='θ', ylabel='ϵ')\n", "ax.text(1.8, 2.5, 'new life', fontsize=14)\n", "ax.text(4.5, 2.5, 'new job', fontsize=14, rotation='vertical')\n", "ax.text(4.0, 4.5, 'stay put', fontsize=14)\n", "plt.show()" ] }, { "cell_type": "markdown", "id": "9a01e541", "metadata": {}, "source": [ "Interpretation:\n", "\n", "- If both job and career are poor or mediocre, the worker will experiment with a new job and new career. \n", "- If career is sufficiently good, the worker will hold it and experiment with new jobs until a sufficiently good one is found. \n", "- If both job and career are good, the worker will stay put. \n", "\n", "\n", "Notice that the worker will always hold on to a sufficiently good career, but not necessarily hold on to even the best paying job.\n", "\n", "The reason is that high lifetime wages require both variables to be large, and\n", "the worker cannot change careers without changing jobs.\n", "\n", "- Sometimes a good job must be sacrificed in order to change to a better career. " ] }, { "cell_type": "markdown", "id": "f99e96a6", "metadata": {}, "source": [ "## Exercises" ] }, { "cell_type": "markdown", "id": "ad03fc7f", "metadata": {}, "source": [ "## Exercise 31.1\n", "\n", "Using the default parameterization in the class `CareerWorkerProblem`,\n", "generate and plot typical sample paths for $ \\theta $ and $ \\epsilon $\n", "when the worker follows the optimal policy.\n", "\n", "In particular, modulo randomness, reproduce the following figure (where the horizontal axis represents time)\n", "\n", "![https://python.quantecon.org/_static/lecture_specific/career/career_solutions_ex1_py.png](https://python.quantecon.org/_static/lecture_specific/career/career_solutions_ex1_py.png)\n", "\n", " \n", "To generate the draws from the distributions $ F $ and $ G $, use `quantecon.random.draw()`." ] }, { "cell_type": "markdown", "id": "977fa43e", "metadata": {}, "source": [ "## Solution to[ Exercise 31.1](https://python.quantecon.org/#career_ex1)\n", "\n", "Simulate job/career paths.\n", "\n", "In reading the code, recall that `optimal_policy[i, j]` = policy at\n", "$ (\\theta_i, \\epsilon_j) $ = either 1, 2 or 3; meaning ‘stay put’,\n", "‘new job’ and ‘new life’." ] }, { "cell_type": "code", "execution_count": null, "id": "05715870", "metadata": { "hide-output": false }, "outputs": [], "source": [ "F = np.cumsum(cw.F_probs)\n", "G = np.cumsum(cw.G_probs)\n", "v_star = solve_model(cw, verbose=False)\n", "T, get_greedy = operator_factory(cw)\n", "greedy_star = get_greedy(v_star)\n", "\n", "def gen_path(optimal_policy, F, G, t=20):\n", " i = j = 0\n", " θ_index = []\n", " ϵ_index = []\n", " for t in range(t):\n", " if optimal_policy[i, j] == 1: # Stay put\n", " pass\n", "\n", " elif greedy_star[i, j] == 2: # New job\n", " j = qe.random.draw(G)\n", "\n", " else: # New life\n", " i, j = qe.random.draw(F), qe.random.draw(G)\n", " θ_index.append(i)\n", " ϵ_index.append(j)\n", " return cw.θ[θ_index], cw.ϵ[ϵ_index]\n", "\n", "\n", "fig, axes = plt.subplots(2, 1, figsize=(10, 8))\n", "for ax in axes:\n", " θ_path, ϵ_path = gen_path(greedy_star, F, G)\n", " ax.plot(ϵ_path, label='ϵ')\n", " ax.plot(θ_path, label='θ')\n", " ax.set_ylim(0, 6)\n", "\n", "plt.legend()\n", "plt.show()" ] }, { "cell_type": "markdown", "id": "3bf1b6c4", "metadata": {}, "source": [ "## Exercise 31.2\n", "\n", "Let’s now consider how long it takes for the worker to settle down to a\n", "permanent job, given a starting point of $ (\\theta, \\epsilon) = (0, 0) $.\n", "\n", "In other words, we want to study the distribution of the random variable\n", "\n", "$$\n", "T^* := \\text{the first point in time from which the worker's job no longer changes}\n", "$$\n", "\n", "Evidently, the worker’s job becomes permanent if and only if $ (\\theta_t, \\epsilon_t) $ enters the\n", "“stay put” region of $ (\\theta, \\epsilon) $ space.\n", "\n", "Letting $ S $ denote this region, $ T^* $ can be expressed as the\n", "first passage time to $ S $ under the optimal policy:\n", "\n", "$$\n", "T^* := \\inf\\{t \\geq 0 \\,|\\, (\\theta_t, \\epsilon_t) \\in S\\}\n", "$$\n", "\n", "Collect 25,000 draws of this random variable and compute the median (which should be about 7).\n", "\n", "Repeat the exercise with $ \\beta=0.99 $ and interpret the change." ] }, { "cell_type": "markdown", "id": "3d427a49", "metadata": {}, "source": [ "## Solution to[ Exercise 31.2](https://python.quantecon.org/#career_ex2)\n", "\n", "The median for the original parameterization can be computed as follows" ] }, { "cell_type": "code", "execution_count": null, "id": "659e052e", "metadata": { "hide-output": false }, "outputs": [], "source": [ "cw = CareerWorkerProblem()\n", "F = np.cumsum(cw.F_probs)\n", "G = np.cumsum(cw.G_probs)\n", "T, get_greedy = operator_factory(cw)\n", "v_star = solve_model(cw, verbose=False)\n", "greedy_star = get_greedy(v_star)\n", "\n", "@njit\n", "def passage_time(optimal_policy, F, G):\n", " t = 0\n", " i = j = 0\n", " while True:\n", " if optimal_policy[i, j] == 1: # Stay put\n", " return t\n", " elif optimal_policy[i, j] == 2: # New job\n", " j = qe.random.draw(G)\n", " else: # New life\n", " i, j = qe.random.draw(F), qe.random.draw(G)\n", " t += 1\n", "\n", "@njit(parallel=True)\n", "def median_time(optimal_policy, F, G, M=25000):\n", " samples = np.empty(M)\n", " for i in prange(M):\n", " samples[i] = passage_time(optimal_policy, F, G)\n", " return np.median(samples)\n", "\n", "median_time(greedy_star, F, G)" ] }, { "cell_type": "markdown", "id": "cd86be55", "metadata": {}, "source": [ "To compute the median with $ \\beta=0.99 $ instead of the default\n", "value $ \\beta=0.95 $, replace `cw = CareerWorkerProblem()` with\n", "`cw = CareerWorkerProblem(β=0.99)`.\n", "\n", "The medians are subject to randomness but should be about 7 and 14 respectively.\n", "\n", "Not surprisingly, more patient workers will wait longer to settle down to their final job." ] }, { "cell_type": "markdown", "id": "317000d2", "metadata": {}, "source": [ "## Exercise 31.3\n", "\n", "Set the parameterization to `G_a = G_b = 100` and generate a new optimal policy\n", "figure – interpret." ] }, { "cell_type": "markdown", "id": "d9726ddc", "metadata": {}, "source": [ "## Solution to[ Exercise 31.3](https://python.quantecon.org/#career_ex3)\n", "\n", "Here is one solution" ] }, { "cell_type": "code", "execution_count": null, "id": "0b325145", "metadata": { "hide-output": false }, "outputs": [], "source": [ "cw = CareerWorkerProblem(G_a=100, G_b=100)\n", "T, get_greedy = operator_factory(cw)\n", "v_star = solve_model(cw, verbose=False)\n", "greedy_star = get_greedy(v_star)\n", "\n", "fig, ax = plt.subplots(figsize=(6, 6))\n", "tg, eg = np.meshgrid(cw.θ, cw.ϵ)\n", "lvls = (0.5, 1.5, 2.5, 3.5)\n", "ax.contourf(tg, eg, greedy_star.T, levels=lvls, cmap=cm.winter, alpha=0.5)\n", "ax.contour(tg, eg, greedy_star.T, colors='k', levels=lvls, linewidths=2)\n", "ax.set(xlabel='θ', ylabel='ϵ')\n", "ax.text(1.8, 2.5, 'new life', fontsize=14)\n", "ax.text(4.5, 1.5, 'new job', fontsize=14, rotation='vertical')\n", "ax.text(4.0, 4.5, 'stay put', fontsize=14)\n", "plt.show()" ] }, { "cell_type": "markdown", "id": "db87b923", "metadata": {}, "source": [ "In the new figure, you see that the region for which the worker\n", "stays put has grown because the distribution for $ \\epsilon $\n", "has become more concentrated around the mean, making high-paying jobs\n", "less realistic." ] } ], "metadata": { "date": 1714442504.2938957, "filename": "career.md", "kernelspec": { "display_name": "Python", "language": "python3", "name": "python3" }, "title": "Job Search V: Modeling Career Choice" }, "nbformat": 4, "nbformat_minor": 5 }