Dynamic programming

Alfred Galichon (NYU & Sciences Po)

'math+econ+code' masterclass on optimal transport and economic applications

With python code examples

© 2018-2022 by Alfred Galichon. Past and present support from NSF grant DMS-1716489, ERC grant CoG-866274 are acknowledged, as well as inputs from contributors listed here.

If you reuse material from this masterclass, please cite as:
Alfred Galichon, 'math+econ+code' masterclass on optimal transport and economic applications, January 2022. https://github.com/math-econ-code/mec_optim

Learning Objectives

References

Movitation

John Rust describes the problem of Harold Zurcher, an engineer who runs a bus fleet as follows:

This problem is a dynamic programming problem. When taking the decision whether to perform the overhaul or not, Zurcher needs to compare the operation cost not only with the cost of overhaul, but also take into account the reduction in operation costs in the future periods.

While in this instance of the problem there is no externality across buses, so the buses could decide in isolation whether to go on maintenance or not, it is not hard to envision a variant of this problem where there are externalities. For instance, one may assume that there is a maximum number of buses that can go on overhaul at the same time.

We shall derive the optimal policy for Harold Zurcher, (somewhat freely) based on Rust's data.

First, let's import the libraries we'll need.

Linear dynamic programming

States and actions

Consider a finite set of states $x\in\mathcal{X}$; and a set of possible actions $y\in\mathcal{Y}$.

It is assumed that at initial time, the mass of agents in state $x$ is $q_{x}$ (exogenous).

Let $\pi_{xy}^{t}$ be the (endogenous) number of individuals who are in state $x$ and choose $y$ ("policy variable").

Because $\pi_{xy}^{t}$ is our decision variable, we shall need to worry about how the computer represents it in the memory: by stacking columns or by stacking rows? this brings us to the important question of vectorization.

Bus example. In our bus maintenance example, one faces a maintenance decision, which is captured by $y\in\mathcal{Y}=\left\{0,1\right\}$, where $y = 0$ is to keep going, and $y=1$ is to perform overhaul. The state $x\in\mathcal{X}=\left\{x_{0},...,x_{I}\right\}$ represents the mileage level of a bus. The transition between states is as follows:

Vectorization and Kronecker products

We will need to represent matrices (such as $U_{x}^{t}$) and 3-dimensional arrays (such as $u_{xy}^{t}$). Under the row-major order (a.k.a. 'C order') used in C and by default in numpy, we will represent a matrix $M_{ij}$ by varying the last index first, i.e. a $2\times2$ matrix will be represented as $vec_C\left(M\right) = M_{11}, M_{12}, M_{21}, M_{22}.$ Likewise, a 2x2x2 3-dimensional array $A$ will be represented by varying the first index first, then the second, i.e.

$vec_C\left(A\right) = A_{111}, A_{112}, A_{121}, A_{122}, A_{211}, A_{212}, A_{221}, A_{222}$.

In numpy, this is implemented by reshape(...).

A very important identity is \begin{align*} vec_C\left(AXB\right) = \left( A\otimes B^\top\right) vec_C\left(X\right), \end{align*} where $vec_C$ is the vectorization under the C (row-major) order, and where the Kronecker product $\otimes$ is defined as follows for 2x2 matrices (with obvious generalization):

\begin{align*} A\otimes B= \begin{pmatrix} a_{11}B & a_{12}B\\ a_{21}B & a_{22}B \end{pmatrix}. \end{align*}

Representation of the decision variable

We shall adopt the convention to store $\pi^t_{xy}$ by taking the indexing ordering to be $(t,x,y)$ under the row-major order. That way we shall store $(\pi^1_{xy})_{xy}$ on top of $(\pi^2_{xy})_{xy}$, etc, and whithin the $(\pi^1_{xy})_{xy}$ block, $(\pi^1_{x_1y})_{y}$ on top of $(\pi^1_{x_2y})_{y}$, etc.

Choice-counting equation

Define $n_{x}^{t}$ be the (endogenous) number of individuals in state $x$ at time $t$. The choice-counting equation expresses that the sum over $y$ of individuals of type $x$ who made choice $y$ is equal to $n_x$, that is:

$ n_{x}^{t} = \sum_{y\in\mathcal{Y}}\pi_{xy}^{t}. $

Clearly, this can we rewritten for each $t$

$ n^t = \begin{pmatrix} 1_{Y}^{\top } & 0 & \cdots & 0 \\ 0 & 1_{Y}^{\top } & \ddots & \vdots \\ \vdots & \ddots & \ddots & 0 \\ 0 & \cdots & 0 & 1_{Y}^{\top } \end{pmatrix} vec_C(\pi^{t}) =\left( I_{X}\otimes 1_{Y}^{\top } \right) vec_C(\pi^{t}), $

and stacking this over $t$ yields

$ n = \begin{pmatrix} I_{X}\otimes 1_{Y}^{\top } & 0 & \cdots & 0 \\ 0 & I_{X}\otimes 1_{Y}^{\top } & \ddots & \vdots \\ \vdots & \ddots & \ddots & 0 \\ 0 & \cdots & 0 & I_{X}\otimes 1_{Y}^{\top }% \end{pmatrix}% vec_C(\pi) = \left( I_{T}\otimes I_{X}\otimes 1_{Y}^{\top } \right) vec_C(\pi). $

Therefore the choice-counting equation reads:

\begin{equation} \left( I_{T}\otimes I_{X}\otimes 1_{Y}^{\top } \right) vec_C(\pi) = n. \end{equation}

Markov transitions

Now assume that if an agent is in state $x$ and takes decision $y$ at time $t-1$, then the agent's state will transition to state $x'$ at time $t$ with probability $P_{x^{\prime}|xy}$, which expresses that among the individual in state $x$ who choose $y$ at time $t-1$, a fraction $P_{x^{\prime}|xy}$ shall transit to state $x^{\prime}$ at time $t$.

$P$ is represented by a matrix whose rows are indexed by $x^{\prime}$ and whose columns are indexed by $xy$. Under the row-major order, the index ordering will be $(x^{\prime},x,y)$.

For $t$ such that $1\leq t\leq T-1$, the Markov transitions are given by

$ n_{x^{\prime}}^{t} = \sum_{x\in\mathcal{X},~y\in\mathcal{Y}}P_{x^{\prime}|xy}\pi_{xy}^{t-1} $, that is

$ n^t = P \pi^{t-1} $, and $n^{1}=q$.

Bus example. In the bus example, assume that the Markov transitions laws are given by:

Define the matrix $L^{\mathcal{X}}_{x^{\prime}|x}$ associated with a transition to the next state, and a reset at the last state, and $R^{\mathcal{X}}_{x^{\prime}|x}$ associated with a reset to the initial state.

for the transitions $x\to x^{\prime}$ by: \begin{align*} L^{\mathcal{X}}=% \begin{pmatrix} 0 & 0 & \cdots & 0 & 1\\ 1 & 0 & \ddots & \ddots & 0\\ 0 & 1 & \ddots & \ddots & 0\\ \vdots & \ddots & \ddots & \ddots & 0 \\ 0 & 0 & 0 & 1 & 0 \end{pmatrix} \text{ and } R^{\mathcal{X}}=% \begin{pmatrix} 1 & 1 & \cdots & 1\\ 0 & \vdots & \ddots & \vdots\\ 0 & \vdots & \ddots & \vdots\\ 0 & 0 & \cdots & 0 \end{pmatrix} \end{align*} Now:

so that $P$ is given by \begin{align*} P=\left( 0.75I_{\mathcal{X}}+0.25L^{\mathcal{X}}\right) \otimes (1,0)

To fix ideas, we shall assume that $\mathcal{X}=\left\{0,1,2\right\}$, that $\mathcal{Y}=\left\{0,1\right\}$ as argued before, and $\mathcal{T}=\left\{0,...,3\right\}$,, and we implement this as follows:

Markov forward equation

Denoting $N_{T}$ the $T \times T$ matrix defined by:

$ N_{T}= \begin{pmatrix} 0 & 0 & \cdots & \cdots & 0\\ 1 & \ddots & \cdots & \cdots & 0\\ 0 & \ddots & \ddots & & \vdots\\ \vdots & \ddots & \ddots & \ddots & \vdots\\ 0 & \cdots & 0 & 1 & 0 \end{pmatrix} $

this rewrites

$ n = (N_{T} \otimes P) \pi + b $

where $b^t_x$ is the vector obtained by stacking column vector $q_x$ on top of $(T-1)X$ zeros.

Therefore the Markov forward equation reads:

\begin{equation} \left(N_{T} \otimes P\right) vec_C(\pi) + b = n. \end{equation}

Population dynamics

Combining the choice-counting equation with the Markov forward equation to substitute out $n$ yields

$$ A ~ vec_C(\pi) = b. $$

where we have defined

$$ A := I_{T}\otimes I_{X}\otimes 1_{Y}^{\top } - N_{T} \otimes P . $$

The implementation is straightforward:

In our example:

Payoffs

Let $u_{xy}^{t}$ be the (exogenous) payoff associated with choice $y\in\mathcal{Y}$ at time $t\in\mathcal{T}=\left\{ 1,...,T\right\} $ in state $x\in\mathcal{X}$, discounted in order to be expressed in zeroth-period equivalent (for example: $u_{xy}^{t}=\beta^{t}u_{xy}$ where $\beta$ is a constant discount factor).

Example. Back to our bus example, we assume there is a fixed cost $C$ associated with overhaul (independent of mileage), while operations costs $c\left( x\right)$ increase with mileage (maintenance, fuel, insurance and costs of unexpected breakdowns). Specifically, assume the following:

Next, we build $u_{xyt}$

In our example:

Intertemporal optimization problem (finite horizon)

Duality

We now have everything ready to write down the intertemporal (primal) optimization problem, which expresses

\begin{align*} \max_{\pi\geq0} & \, u^{\top}\pi\\ s.t.~ & A\pi=b~\left[U\right] \end{align*}

while the dual problem is given by

\begin{align*} \min_{U} & \, b^{\top}U\\ s.t.~ & A^{\top} U\geq u~\left[\pi \geq 0\right] . \end{align*}

We shall write explicitely both problems to interpret the dual, but first, let's implement.

In our example:

Primal problem: central planner's problem

The central planner's problem is:

\begin{align*} \max_{\pi_{xy}^{t}\geq0} & \sum_{x\in\mathcal{X},~y\in\mathcal{Y},~t\in\mathcal{T}}\pi_{xy}^{t}u_{xy}^{t} \\ s.t. & \sum_{y^{\prime}\in\mathcal{Y}}\pi_{xy^{\prime}}^{0}=q_{x}~\left[U_{x}^{1}\right] \\ & \sum_{y^{\prime}\in\mathcal{Y}}\pi_{x^{\prime}y^{\prime}}^{t}=\sum_{x\in\mathcal{X},~y\in\mathcal{Y}}P_{x^{\prime}|xy}\pi_{xy}^{t-1}~\forall t\in\mathcal{T}\backslash\left\{ 0\right\} ~\left[ U_{x^{\prime}}^{t}\right] \end{align*}

Dual problem: dynamic programming

We have introduced $U_{x}^{t}$ the Lagrange multiplier associated with the constraints at time $t$. The dual problem is:

\begin{align*} \min_{U_{x}^{t},~t\in\mathcal{T},~x\in\mathcal{X}} & \sum_{x\in\mathcal{X}}q_{x}U_{x}^{0} \\ s.t.~ & U_{x}^{t}\geq u_{xy}^{t}+\sum_{x^{\prime}}U_{x^{\prime}}^{t+1}P_{x^{\prime}|xy}~\forall x\in\mathcal{X},~y\in\mathcal{Y},~t\in\mathcal{T}\backslash\left\{ T - 1\right\} \\ & U_{x}^{T-1}\geq u_{xy}^{T-1}~\forall x\in\mathcal{X},y\in\mathcal{Y} \end{align*}

We shall see that $U_{x}^t$ represents the intertemporal payoff of being in state $x$ at time $t$, while the constraints is a Bellman equation.

Complementary slackness and Bellman's equation

By complementary slackness, we have

\begin{align*} \pi_{xy}^{t}>0\Longrightarrow U_{x}^{t}=u_{xy}^{t}+\sum_{x^{\prime}}U_{x^{\prime}}^{t+1}P_{x^{\prime}|xy} \end{align*}

whose interpretation is immediate: if $y$ is the optimal choice in state $x$ at time $t$, then the intertemporal payoff of $x$ at $t$ is the sum of her myopic payoff $u_{xy}^{t}$ and her expected payoff at the next step.

As a result, the dual variable is called intertemporal payoff in the vocable of dynamic programming. The relation yields Bellman's equation, verified as soon as $n^t_x>0$:

\begin{align*} U_{x}^{t}=\max_{y\in\mathcal{Y}}\left\{ u_{xy}^{t}+\sum_{x^{\prime}}U_{x^{\prime}}^{t+1}P_{x^{\prime}|xy}\right\}, \end{align*}

It is easy to see that if $U$ satisfies Bellman's equation for all $(x,t)$, then it solves the dual program.

Backward-forward induction

But there is in fact a much faster way to compute the primal and dual solutions without having to use the full power of a linear programming solver. Along with the fact that $U^{T}=0$, Bellman's equation implies that there is a particularly simple method to obtain the dual variables $U^{t}$, by solving recursively backward in time, from $t=T-1$ to $t=0$. This method is called backward induction:


Algorithm [Backward induction]

  1. Set $U^{T}=0$

  2. For $t=T-1$ down to $0$, set $U_{x}^{t}:=\max_{y\in\mathcal{Y}}\left\{u_{xy}^{t}+\sum_{x^{\prime}}U_{x^{\prime}}^{t+1}P_{x^{\prime}|xy}\right\}$.

We implement as follows:

In our example:

The primal variables $\pi^{t}$ are then deduced also by recursion, but this time forward in time from $t=1$ to $t=T-1$, by the so-called forward induction method:


Algorithm [Forward induction]

  1. Set $b^{0}=q$ and compute $\left( U^{t}\right)$ by backward induction.

  2. For $t=0$ up to $T-1$, pick $\pi^{t}$ such that $\pi_{xy}^{t}/n_{x}^{t}$ is a probability measure supported in the set

\begin{align*} \left\{ y:U_{x}^{t}=u_{xy}^{t}+\sum_{x^{\prime}}U_{x^{\prime}}^{t+1}P_{x^{\prime}|xy}\right\} . \end{align*}
  1. Set $n_{x^{\prime}}^{t+1}:=\sum_{x\in\mathcal{X},~y\in\mathcal{Y}}P_{x^{\prime}|xy}\pi_{xy}^{t-1}$

We implement as follows:

In our example:

Remarks

  1. The dual variable is $U$ not necessarily unique (if $(x,t)$ is not visited, $U^t_x$ can take typically several values); the primal variable is not either, as there may be ties between several states.

  2. The computation by the combination of the backward and forward algorithms is much faster than the computation by a black-box linear programming solver.

  3. However, as soon as we introduce capacity constraints, the computation by backward induction no longer works, and the linear programming formulation is necessary, as we shall now see.

Population constraints

Let us now assume that for each category $x$ at most $k_y$ individuals can take choice $y$ at each time, where $\sum_y k_y \geq \sum_x q_x$. An additional constraint is therefore that for all $t\in\mathcal{T}$, $x \in \mathcal{X}$ and $y \in \mathcal{Y}$, one should have:

$$ \pi^t_{xy} \leq k_y $$

Exercise.

  1. Write down (in matrix form) how the primal problem is modified.
  2. In the example above, assume $k_0=+\infty$ and $k_1 = 1$ and compute the problem using LP.
  3. Can we use the backward-forward algorithm?

Solution to the exercise

With a constraint of the form $B\pi\leq m$, the primal problem then writes

\begin{align*} \max_{\pi\geq0} & u^{\top}\pi\\ s.t.~ & A \pi=b~\left[ U\right] \\ & B \pi\leq m~\left[ \Lambda\right] \end{align*}

whose dual is

\begin{align*} \min_{U,\Lambda\geq0} & b^{\top}U+m^{\top}\Lambda\\ s.t.~ & A^\top U+B^\top\Lambda\geq u~\left[ \pi\right] \end{align*}

The dual becomes \begin{align*} \min_{U_{x}^{t},\lambda_{y}^{t}\geq0} & \sum_{x\in\mathcal{X}}n_{x}U_{x}% ^{1}+\sum_{x\in\mathcal{X}}\sum_{t\in\mathcal{T}}m_{y}\lambda_{y}^{t}\\ s.t.~ & U_{x}^{t}\geq u_{xy}^{t}-\lambda_{y}^{t}+\sum_{x^{\prime}% }U_{x^{\prime}}^{t+1}P_{x^{\prime}|xy}~\forall x\in\mathcal{X},~y\in \mathcal{Y},~t\in\mathcal{T}\backslash\left\{ T\right\} \nonumber\\ & U_{x}^{T}\geq u_{xy}^{T}~\forall x\in\mathcal{X},y\in\mathcal{Y}\nonumber \end{align*}

and $\lambda_{y}^{t}$ interprets as the shadow price of alternative $y$ at time $t$.

Solution to the exercise. The constraints $\pi^t_{xy} \leq k_y$ express as $$vec_C(\pi) \leq 1_{XT} \otimes k$$ However, when the upper bound is $+\infty$, we'd rather drop the corresponding constraints. In which case, the matrix form becomes $$(I_{XT} \otimes (0,1)) vec_C(\pi) \leq 1_{XT}$$ which we code as:

Another plausible constraint is to assume that the maximum of buses that can go on maintenance all together is $k$, say e.g. $k=1.5$. This constraint now writes $$\left( I_{T}\otimes 1_{X}^{\top }\otimes (0,1)\right) vec\left( \pi \right) \leq k 1_{T},$$ and is coded as:

In these cases, the linear programming solver is our only way to compute the solution. The backward induction algorithm would not apply because of the capacity constraint.