Introducing NumPy

Loading the Library

NumPy (Numerical Python) is the foundation for numerical computing in Python. Its main contribution is the array — a data structure optimized for mathematical operations. Unlike Python lists, NumPy arrays apply operations element-wise without needing loops, which makes code both faster and easier to read.

To use NumPy we import it first.

import numpy as np

It is common to import the library and call it np. For example, if we want to compute the natural logarithm of 2, we can now type:

np.log(2)
np.float64(0.6931471805599453)

All functions in the library are accessed by typing the library name, a dot, and the function name. Note that in NumPy — as in most programming languages — the natural logarithm is called log.

You may notice that the output appears as np.float64(0.693...) rather than just 0.693.... This is NumPy indicating the type of the result. The value is correct; it’s just displayed with its type label. If you want to see just the number, wrap it with float():

float(np.log(2))
0.6931471805599453

This is purely cosmetic — np.float64 values behave identically to regular Python floats in any calculation.

The exponential function works the same way. The following computes \(e^{3} = \exp(3)\).

np.exp(3)
np.float64(20.085536923187668)

NumPy Arrays

Another great use of NumPy is that it allows us to define numerical arrays. Say for example that we want to compute the value of the previous bond for several interest rates. An efficient way to do it is the following.

First, let’s define the array of rates for which we want the value of the bond.

rates = np.array([0.04, 0.05, 0.06, 0.07, 0.08])

We can now check that the variable rates contains indeed our values.

rates
array([0.04, 0.05, 0.06, 0.07, 0.08])

In Python, we can access individual values of the array by querying its position. We must be careful, though, as in Python the first element in an array is the element number 0. This way of accessing the elements of an array is commonly used in other programming languages such as C.

Therefore, we can extract the first element of the array by typing:

rates[0]
np.float64(0.04)

And the second element is:

rates[1]
np.float64(0.05)

The last element can be accessed by typing:

rates[4]
np.float64(0.08)

Python also supports negative indexing: rates[-1] always returns the last element regardless of array length, which is more robust than counting positions manually.

rates[-1]
np.float64(0.08)

We are now in position to use our new tools. We are going to slightly re-write our bond pricing function as follows.

def pv_bond_np(c, y, F, T):
    pv = (F * (c/2) / (y/2)) * (1 - 1 / (1 + (y/2))**(2*T)) + F / (1 + y/2)**(2*T)
    return np.round(pv, 2)

The only change is replacing Python’s built-in round with NumPy’s np.round, which can operate on arrays.

pv_bond_np(0.05, rates, 1000, 5)
array([1044.91, 1000.  ,  957.35,  916.83,  878.34])

We pass the array rates as the second argument y. Because NumPy applies arithmetic operations element-wise, the function runs on every rate in the array at once — no loop needed. The output is an array of bond prices, one for each YTM in rates. Notice that higher yields produce lower prices, consistent with the inverse relationship between bond prices and interest rates.

Practice Problems

Problem 1 Write a function \(f(x) = x^{2} - 2 x + 3\) and compute the values of the function when applied to a NumPy array \(x = [-4, -3, -2, -1, 0, 1, 2, 3, 4]\).

Solution

The solution below assumes that you have already imported NumPy by running import numpy as np.

def my_func(x):
    y = x**2 - 2*x + 3
    return y

x = np.array([-4,-3,-2,-1,0,1,2,3,4])
my_func(x)
array([27, 18, 11,  6,  3,  2,  3,  6, 11])