A major topic when we need to work with functions
Important for dynamic programming
In numerical computing we often work with arrays
At each stage we can store the modified array, because it only requires finite memory
Example:
Returning to the topic of Markov chains (see this lecture)
p be a Markov matrix q be a distribution (probability mass function) If the current state X[t] has distribution q, then X[t+1] has
distribution np.dot(q, p)
So if q is the distribution of the initial state X[0], then the
time distribution of X[t] can be computed via
for j in range(t):
q = np.dot(q, p) # The final value of q is the distribution of X[t]
At each iteration we only need to store the finite array q, so there's no problem here
But now suppose we want to work with functions, rather than arrays
Consider this function:
def f(x):
return np.sin(x) - 2 * np.cos(x) # Assume numpy imported as np
This function f is easily stored on the computer
However, suppose we want to
In practice this might be problematic, because functions are inherently infinite
Can't store the newly created functions at each step
Example:
In the next lecture we will study value function iteration
w
w' according to
where rho, f, U and phi are known functions/constants
The number of possible y is infinite, so we can't evaluate at every y in finite time
Instead we approximate w' as follows
w' on a finite grid of points, and store these values
w' using this data
More generally, to approximate a function we need
This is naturally done using OOP, since we are combining data and behavior (reconstruction of the function)
We will concentrate on simple approximation schemes suitable for dynamic programming
One of the simplest approximation schemes is step functions
Useful because
A one-dimensional step function s is defined by
X (the grid), and
Y with len(Y) = len(X)
The function s is the (broken) blue line in this figure
A simplistic implementation of s is as follows
def s(x):
if x < X[0]: # x < first element of grid
y = 0
elif x >= X[-1]: # x >= last element of grid
y = Y[-1]
else:
for i in range(len(X)-1):
if X[i] <= x < X[i+1]:
y = Y[i]
return y
Sometimes we want to evaluate the expectation of s(Z) where
Z is a random variable
Z is given by F
For example, the expectation of the simple function in the figure is
Y[0] * (F(X[1]) - F(X[0])) + Y[1] * (F(X[2]) - F(X[1])) + Y[2] * (1 - F(X[2]))
Problem:
Design a class StepFun such that an instance represents a particular step function
X and Y
__call__ method that evaluates s(x) for any x
expectation method that F
s(Z) when F is the distribution of X
Try to be efficient (i.e., avoid loops)
Hint: If X is an increasing array, then X.searchsorted(x, side='right') - 1
returns the largest i such that X[i] <= x
Solution:
import numpy as np
class StepFun:
def __init__(self, X, Y):
"""
Parameters:
* X is an increasing array of length n
* Y is an array of length n
Implements a step function s, where
s(x) = sum_{i=0}^{n-1} Y[i] 1{X[i] <= x < X[i+1]}
where X[n] := infty
"""
self.X, self.Y = X, Y
def __call__(self, x):
"Evaluate the step function at x."
if x < self.X[0]:
return 0.0
i = self.X.searchsorted(x, side='right') - 1
return self.Y[i]
def expectation(self, F):
"Computes expectation of s(Y) when F is the cdf of Y."
probs = np.append(F(self.X), 1)
return np.dot(self.Y, probs[1:] - probs[:-1])
Now we turn to another approximation scheme: piecewise linear interpolation
Linear interpolation is implemented by the NumPy/SciPy interp function
Here's an example of usage
import numpy as np
import matplotlib.pyplot as plt
xvals = np.linspace(-2, 2, 100)
yvals = np.sin(xvals)
plt.plot(xvals, yvals, 'k-')
eval_points = [-2, -1, 0, 1, 2]
plt.plot(eval_points, np.interp(eval_points, xvals, yvals), 'r-')
plt.show()
Generates this graph
When we do dynamic programming, we will use a simple class which wraps the function interp
## Filename: lininterp.py
## Author: John Stachurski
from scipy import interp
class LinInterp:
"Provides linear interpolation in one dimension."
def __init__(self, X, Y):
"""Parameters: X and Y are sequences or arrays
containing the (x,y) interpolation points."""
self.X, self.Y = X, Y
def __call__(self, z):
return interp(z, self.X, self.Y)
The purpose of the class is to store the interpolation data locally (i.e., as instance data)