Scroll back to top
Code outputs are displayed below code boxes.
We'll go through some examples here to refresh the basics of numpy. There are also plenty of other guides online:
First, to use a package we need to import it.
Text successfully copied to clipboard!
import numpy as np
Question: Why bother with numpy?
Answer: Numpy arrays are faster and more efficient than lists when working with numerical data.
Let's compare the running time for a basic operation in pure python and numpy. The following code blocks create two random matrices \( A \) and \( B \), then compute \( C = A B \) (you don't need to worry about understanding the code at this time).
The first code block is with pure python using lists (no numpy).
Text successfully copied to clipboard!
import random
import time
def create_rand_matrix(n):
# Create random matrix without numpy
M = []
for i in range(n):
row = []
for j in range(n):
row.append(random.random())
M.append(row)
return M
start = time.perf_counter()
n = 100
A = create_rand_matrix(n)
B = create_rand_matrix(n)
# Compute C = AB
C = []
for i in range(n):
row = []
for j in range(n):
sum = 0
for k in range(n):
sum += A[i][k] * B[k][j]
row.append(sum)
C.append(row)
stop = time.perf_counter()
print(stop - start)
0.3338270999993256
The same thing using numpy arrays.
Text successfully copied to clipboard!
start = time.perf_counter()
n = 100
A = np.random.random((n, n))
B = np.random.random((n, n))
C = A @ B
stop = time.perf_counter()
print(stop - start)
0.04226900000139722
Numerous ways of creating arrays are available.
Text successfully copied to clipboard!
vals_list = [1, 3, 2, 8]
vals_array = np.array(vals_list)
print("vals_list: ", vals_list)
print("vals_array: ", vals_array)
vals_list: [1, 3, 2, 8]
vals_array: [1 3 2 8]
Evenly spaced numbers in a interval (meant for use with an integer step size). Examples:
Text successfully copied to clipboard!
print(np.arange(10)) # stop
print(np.arange(4,12)) # start and stop
print(np.arange(4,12,2)) # start, stop, and step
[0 1 2 3 4 5 6 7 8 9]
[ 4 5 6 7 8 9 10 11]
[ 4 6 8 10]
Evenly spaced numbers in an interval. Examples:
Text successfully copied to clipboard!
print(np.linspace(0,1,11)) #start, stop, step
print(np.linspace(0,10,11))
[0. 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1. ]
[ 0. 1. 2. 3. 4. 5. 6. 7. 8. 9. 10.]
Note that unlike np.arange(), here the last number is included, and the third parameter is the number of steps.
Often need to initialize to zeros or vector of all 1s
Text successfully copied to clipboard!
print(np.zeros(5))
print(np.ones(4))
print(np.zeros(5))
print(np.ones(4))
Recall, Python is dynamically typed. Types are changed automatically as needed. And, lists can hold anything. A single list could hold strings and integers.
What about arrays? Numpy arrays are statically typed.
So, what are the data types of the arrays we created above? What are the available datatypes? How do we specify what datatype we want?
Text successfully copied to clipboard!
vals_list = [1,3,2,8]
vals_array = np.array(vals_list)
vals_arrayf = np.array(vals_list, dtype=float)
print("vals_array: ", vals_array)
print("vals_arrayf: ", vals_arrayf)
print(type(vals_list))
print(type(vals_array))
print(type(vals_arrayf))
print(vals_array.dtype)
print(vals_arrayf.dtype)
vals_array: [1 3 2 8]
vals_arrayf: [1. 3. 2. 8.]
<class 'list'>
<class 'numpy.ndarray'>
<class 'numpy.ndarray'>
int32
float64
The dtype argument is valid for most array-creation functions, including numpy.zeros, np.ones, and np.arange.
In Python3, the dtype of an array that results from mathematical operations will automatically adjust to whatever is sensible.
Text successfully copied to clipboard!
print('integers: ', vals_array)
print('more integers: ', vals_array * 3)
print('floats: ', vals_array / 3)
integers: [1 3 2 8]
more integers: [ 3 9 6 24]
floats: [0.33333333 1. 0.66666667 2.66666667]
You can also copy an array and change the dtype.
Text successfully copied to clipboard!
arr = np.arange(10.0)
x = arr.astype(int)
print('arr: ', arr)
print('x: ', x)
arr: [0. 1. 2. 3. 4. 5. 6. 7. 8. 9.]
x: [0 1 2 3 4 5 6 7 8 9]
Now that we actually have arrays, how do we get things from them? Indexed from 0, bracket notation for accessing
Text successfully copied to clipboard!
print(vals_arrayf)
print(vals_arrayf[0])
[1. 3. 2. 8.]
1.0
Negative indexing is also allowed. An index of -1 will return the last element, an index of -2 returns the second to last, and so on.
Text successfully copied to clipboard!
print(vals_arrayf)
print("last element: ", vals_arrayf[-1])
print("second to last element: ", vals_arrayf[-2])
[1. 3. 2. 8.]
last element: 8.0
second to last element: 2.0
What if I want a section of an array? Array slicing.
Text successfully copied to clipboard!
print(vals_arrayf[1:3]) #starting index (inclusive): stopping index (exclusive)
print(vals_arrayf[1:2])
[3. 2.]
[3.]
In addition to a start and end, you can also choose a step for the slice.
Text successfully copied to clipboard!
print(vals_arrayf)
print(vals_arrayf[::2]) # even indices
print(vals_arrayf[1::2]) # odd indices
print(vals_arrayf[::-1]) # handy way to reverse an array
[1. 3. 2. 8.]
[1. 2.]
[3. 8.]
[8. 2. 3. 1.]
You need to be careful with numpy arrays if you are
You might be in for a nasty surprise if you change an element.
Text successfully copied to clipboard!
simple = np.arange(5)
small = simple[:2]
print(simple)
print('')
print(small)
print('')
small[0] = 7
print(small)
print('')
print(simple) # shouldn't have changed, right?
[0 1 2 3 4]
[0 1]
[7 1]
[7 1 2 3 4]
This happens because small is something called a "view" of simple, rather than a copy.
This helps numpy save memory and speed up your program, but it can lead to tricky bugs if it is not your intent.
In general, it can be difficult to tell whether something will be a view or a copy.
Functions also do not make copies of their input arrays.
Text successfully copied to clipboard!
def foo(x): # notice that x is not returned
x[0] = 100
foo(simple)
print(simple)
[100 1 2 3 4]
If you think you are accidentally changing your array elsewhere in your code, you can copy it to be on the safe side. This will be slow your program down and use more memory, but it can help debugging and save a lot of headaches.
Text successfully copied to clipboard!
simple = np.arange(5)
print('before:')
print(simple)
my_copy = simple[:2].copy()
my_copy[1] = 10
foo(simple.copy())
print('after:')
print(simple)
before:
[0 1 2 3 4]
after:
[0 1 2 3 4]
There are trigonometric functions we will use that are built into Numpy
Text successfully copied to clipboard!
theta = np.pi/4
print(f"sin(pi/4) = {np.sin(theta):.3f}")
print(f"cos(pi/4) = {np.cos(theta):.3f}")
print(f"tan(pi/4) = {np.tan(theta):.3f}")
#You can also call inverse functions
x = 1e-3
y = np.arcsin(x) #returns y in radians
x1 = -4
y1 = -3
theta1 = np.arctan(y1/x1) #does not choose correct quadrant
theta2 = np.arctan2(y1,x1) #chooses correct quadrant
print(f"Note that these angles are in different quadrants: {theta1:.5f}, {theta2:.5f} (rds)")
sin(pi/4) = 0.707
cos(pi/4) = 0.707
tan(pi/4) = 1.000
Note that these angles are in different quadrants: 0.64350, -2.49809 (rds)
You've probably noticed by now that Numpy's trigonometric functions either input/output angles in radians. There are multiple ways to convert an angle to radians (and back to degrees)
Text successfully copied to clipboard!
alpha_deg = 180
#code converts to radians, comments show revertion to degrees
alpha_rd1 = np.deg2rad(alpha_deg) #np.rad2deg()
alpha_rd2 = np.radians(alpha_deg) #np.degrees()
alpha_rd3 = alpha_deg*np.pi/180 #*180/np.pi
You can also use Numpy for vector arithmetics...
Text successfully copied to clipboard!
vector_a = np.array([1,0,0])
vector_b = np.array([0,1,0,])
#c = a ● b
vector_c = np.dot(vector_a, vector_b) #recall that would be the same as np.dot(vector_b, vector_a)
#d = a x b
vector_d = np.cross(vector_a, vector_b) #order matters here
... or linear algebra. Here's an example of solving a system of linear equations using two different methods.
Text successfully copied to clipboard!
A = np.array([[1, 1, 0],
[0, -3, 1],
[2, 1, -3]])
b = np.array([6, 7, 5])
#Ax = b
x1 = np.linalg.solve(A,b)
#x = A^-1 b
x2 = np.linalg.inv(A) @ b
#or finding the magnitude/length of a vector
b_magnitude = np.linalg.norm(b)