Lecture 3.1
Guoliang Ma
The Chow Institute, 2026
Preserved from the source lecture; these are historical discussion prompts, not the current course schedule.
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
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.
source: https://towardsdatascience.com/introducing-numpy-part-1-understanding-arrays-3f6fecc97e3d/

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.
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)
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
random.normal for random ndarrays with entries normally distributed
random.randint for random ndarrays with entries randomly drwan
We can use numpy.arange to make a range-like object.
We can use numpy.linspace to create sequence.
Create a 3-D ndarray. Each entry of this ndarray should follow a normal distribution with mean 5 and standard deviation 3.
We can use numpy.arange to make a range-like object.
We can use numpy.linspace to create sequence.
Create a 3-D ndarray. Each entry of this ndarray should follow a normal distribution with mean 5 and standard deviation 3.
What’s the difference between numpy.arange and numpy.linspace?
source: https://pabloinsente.github.io/intro-numpy-fundamentals

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.
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")
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.")
source: https://www.britannica.com/science/linear-algebra

In linear algebra, you learn how to add two matrices, as long as the dimensions match:
Use lists to implement the above matrix addition.
Use numpy to implement the above matrix addition.
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
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
In linear algebra, we also learned how to transpose a matrix:
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
There is a numpy.transpose. What’s the difference with .T?
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)
source: https://pabloinsente.github.io/intro-numpy-fundamentals

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
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.
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
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))
Example 3.1.4.1
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))
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.
An example
Read the documentation and write your own function to compute . Then test the speed of the vectorized function.
source: https://www.dataquest.io/blog/settingwithcopywarning/

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}")
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