Lecture 3.1

NumPy

Python and Big Data in Economics

Guoliang Ma
The Chow Institute, 2026

Original classroom announcement

Preserved from the source lecture; these are historical discussion prompts, not the current course schedule.

Midterm discussion

  • May 6 or May 8?
  • Is a computer-based test feasible?

Homework questions

  • Grading?
  • Confusions?

What you will learn

A new and more general data structure: array

Introductory numpy data structure: ndarray

Basic numpy functions

You’ll need to read a lot of function documents.

Matrix operations with numpy

the broadcast mechanism

*accelerating by vectorization

3.1 The NumPy module

Install NumPy and pandas into the Python environment used by your notebook. In an IPython/Jupyter notebook cell:

%pip install numpy pandas

In a terminal, with the intended environment activated:

python -m pip install numpy pandas

We will mostly use pandas, but NumPy provides important tools for working with arrays. This lecture covers a small part of the library.

NumPy’s official beginner’s guide

3.1.1 ndarray

source: https://towardsdatascience.com/introducing-numpy-part-1-understanding-arrays-3f6fecc97e3d/

NumPy: original illustration, slide 5.

3.1.1 The numpy module (ndarrays)

We've learned that a complex data type can hold several elements (e.g., a list or a tuple). A similar idea of putting numbers into a sequence gives rise to the array type. According to the numpy document,

"In computer programming, an array is a structure for storing and retrieving data."

We emphasize two features of numpy arrays:

They have fixed sizes (number of elements).

The elements must be of the same type.

3.1.1 The numpy module (ndarrays)

Let’s create some arrays

We can use the dtype (stands for data type) parameter to specify the type of the elements.

import numpy as np

a = np.array([1, 2, 3])
b = np.array([1, 2, 'a'])
c = np.array([a, b])
# d = np.array([a, 'python'])
a = np.array([1, 2, 3])
print(a)
a = np.array(a, dtype=float)
print(a)

3.1.1 The numpy module (ndarrays)

The numpy module also provides us with some useful functions to create special ndarrays:

ones that creates all-one ndarrays

zeros that creates all-zero ndarrays

eye that creates diagonal matrices

random.random for random ndarrays with entries 𝑥:0<𝑥<1

random.normal for random ndarrays with entries 𝑥 normally distributed

random.randint for random ndarrays with entries randomly drwan

3.1.1 The numpy module (ndarrays)

We can use numpy.arange to make a range-like object.

We can use numpy.linspace to create sequence.

In-class exercise 1

Create a 3-D ndarray. Each entry of this ndarray should follow a normal distribution with mean 5 and standard deviation 3.

3.1.1 The numpy module (ndarrays)

We can use numpy.arange to make a range-like object.

We can use numpy.linspace to create sequence.

In-class exercise 1

Create a 3-D ndarray. Each entry of this ndarray should follow a normal distribution with mean 5 and standard deviation 3.

In-class exercise 2

What’s the difference between numpy.arange and numpy.linspace?

3.1.2 Vectorization

source: https://pabloinsente.github.io/intro-numpy-fundamentals

NumPy: original illustration, slide 11.

3.1.2 The numpy module (vectorization)

What makes numpy so popular and important in Python data analysis is its power in math operations, especially matrix algebra. This makes numpy efficient in dealing with vector and matrix operations. Some authors even wrote

"NumPy is all about vectorization."

numpy is implemented in the C programming language and is very fast even though we write code in Python.

3.1.2 The numpy module (vectorization)

Example 3.1.2.1 How much faster is numpy than list?

import numpy as np
import time
size = 10_000_000

python_list = list(range(size))
numpy_array = np.arange(size, dtype=np.int64)

# List timing
stime = time.perf_counter()
python_list_squared = [x**2 for x in python_list]
etime = time.perf_counter()
list_time = etime - stime
print(f"Time taken by list: {list_time:.5f} seconds")

3.1.2 The numpy module (vectorization)

Example 3.1.2.1 (Continued)

# numpy timing
stime = time.perf_counter()
numpy_array_squared = numpy_array**2
etime = time.perf_counter()
numpy_time = etime - stime
print(f"Time taken by NumPy array: {numpy_time:.5f} seconds")

# results
print(f"NumPy is {list_time / numpy_time:.2f} times faster than lists in this example.")

3.1.3 Linear algebra

source: https://www.britannica.com/science/linear-algebra

NumPy: original illustration, slide 15.

3.1.3 The numpy module (matrix algebra)

In linear algebra, you learn how to add two matrices, as long as the dimensions match:

𝑀1=(123456), 𝑀2=(654321)

𝑀1+𝑀2=(777777)

In-class exercise 3

Use lists to implement the above matrix addition.

Use numpy to implement the above matrix addition.

3.1.3 The numpy module (matrix algebra)

We can add a one-dimensional array across the rows of a matrix

The mechanism that numpy introduce when we use math operators (+, -, *, /, etc.) is called broadcasting. It means that the “smaller” ndarray is broadcast to each element of the “larger” ndarray.

m1 = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
v1 = np.array([1, 1, 1])
m1 + v1

3.1.3 The numpy module (matrix algebra)

In-class exercise 4

What does it mean by smaller and larger?

What if the size does not match any of the dimensions?

m = np.array([[1, 2], [4, 5], [7, 8]])
v = np.array([1, 1])
A = np.ones((2, 3))
B = np.ones((2,))
x = np.ones((4, 2, 3))
y = np.ones((4, 3))
m1 = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
v1 = np.array([1, 1])
m1 + v1

3.1.3 The numpy module (matrix algebra)

In linear algebra, we also learned how to transpose a matrix:

𝑀1=(123456), 𝑀1T=(142536)

This can also be done with numpy:

m1 = np.array([
    [ 0, 1, 2, 3],
    [ 4, 5, 6, 7],
    [ 8, 9, 10, 11]
  ])

m1T = m1.T

3.1.3 The numpy module (matrix algebra)

In-class exercise 4

There is a numpy.transpose. What’s the difference with .T?

3.1.3 The numpy module (matrix algebra)

The numpy.ndarray is even more flexible, we can use the .reshape method to change the shape of a numpy.ndarray.

m11 = m1.reshape(12)
print(m11, ":", m11.shape)

m12 = m1.reshape([12,])
print(m12, ":", m12.shape)

m13 = m1.reshape([12, 1])
print(m13.T, ":", m13.shape)

m14 = m1.reshape([2, 6])
print(m14.T, ":", m14.shape)

3.1.4 Vectorization (again)

source: https://pabloinsente.github.io/intro-numpy-fundamentals

NumPy: original illustration, slide 22.

3.1.4 Revisit vectorization

numpy is fasters because of its vectorized nature. recall that we once tried to add a number to a list:

and there was an error.

But with numpy.ndarray, we can easily add a scalar to an array.

lst = [1, 2, 3]
lst + 4

3.1.4 Revisit vectorization

More generally, we introduce some universal functions (ufuns) of numpy. The reason is not only their speed but also their ease of use. We've seen that list operations are much slower than ndarray. Now let's focus our attention on the comparison between loops and ufuns, both applied to a ndarray. (Part of the code are excerpts from Python Data Science Handbook by Jake Vanderplas.)

Let's start with "normal" functions.

3.1.4 Revisit vectorization

Example 3.1.4.1

def timer_100(func):
    times = []
    def wrapper(*args, **kwargs):
        for i in range(100):
            stime = time.perf_counter()
            func(*args, **kwargs)
            etime = time.perf_counter()
            times.append(etime - stime)
        avg = np.mean(times)
        std = np.std(times)
        print(f"The average run time is {avg:.6f}s; the std is {std:.6f}s")
    return wrapper

3.1.4 Revisit vectorization

Example 3.1.4.1

@timer_100
def compute_reciprocals(nums):
    output = np.empty(len(nums))
    for i in range(len(nums)):
        output[i] = 1.0 / nums[i]
    return output


compute_reciprocals(np.random.randint(1, 10, size=10_000))

3.1.4 Revisit vectorization

Example 3.1.4.1

In-class exercise 7

Although we saw that the time difference was huge between compute_reciprocals and numpy_reciprocals, we haven't checked if the answers are correct. Verify the correctness of these two functions.

@timer_100
def numpy_reciprocals(nums):
    return 1.0 / nums


numpy_reciprocals(np.random.randint(1, 10, size=10_000))

3.1.4 Universal functions and vectorize

NumPy’s universal functions (ufuncs) operate elementwise. Examples include abs, sin, cos, tan, arcsin, arccos, arctan, exp, and log.

expm1(x) computes exp(x) − 1, and log1p(x) computes log(1 + x), with improved accuracy near zero.

np.vectorize wraps a Python callable so that it accepts array inputs. It is a convenience interface, not a way to compile a function or automatically make its loop fast.

NumPy: vectorize

3.1.4 Revisit vectorization

An example

In-class exercise 8

Read the documentation and write your own function to compute 𝑓(𝑥)=1/(1 + 𝑥2). Then test the speed of the vectorized function.

3.1.4 Revisit vectorization

In-class exercise 9

Read the documentation of the min and mean functions. Given a ndarray

select those entries whose values are greater than 3 (the mask);

find the min value of each row;

find the mean of each column.

arr = np.array([
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
])

3.1.5 View and Copy

source: https://www.dataquest.io/blog/settingwithcopywarning/

NumPy: original illustration, slide 31.

3.1.5 View vs. copy

A very complex phenomenon of numpy is the difference between a view and a copy. If we use analogy, a view offers us a way to look at the data so the data remains itself (no subdata is taken out). A copy, on the contrary, means we create a subdata and give it a new name. The main difference between a view and a copy is when we change their contents. For example, y is a view of part of x below:

x = np.arange(10)
y = x[1:3]
print(f"before changing x, y: {y}")
x[1:3] = [10, 11]
print(f"after changing x, y: {y}")

3.1.5 View vs. copy

We can check if y is a view of x using the .base attribute:

We will not talk too much for now. You can find more of it from here.

Further readings:

https://numpy.org/doc/2.0/user/basics.copies.html

https://scipy-cookbook.readthedocs.io/items/ViewsVsCopies.html

https://stackoverflow.com/questions/47181092/numpy-views-vs-copy-by-slicing

y.base is x