Go to top, next, previous, or johnstachurski.net

Function Approximation

A major topic when we need to work with functions

Important for dynamic programming

The Problem with Finite Memory

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)

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



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

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

Step Functions

One of the simplest approximation schemes is step functions

Useful because

A one-dimensional step function s is defined by

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

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

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]) 

Linear Interpolation

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)