import numpy as npLecture 4 - NumPy arrays
Programming for Engineers
1 Lecture 4 - NumPy
1.1 Introduction to NumPy
The NumPy library enhances the Python with support for
numerical operations. It is especially useful for large
(multi-dimensional) arrays and matrices. Much of it is actually
programmed in C and uses vectorization (parallelization) for efficient
computations. The arrays are typed, which puts a restriction on what can
be stored in them (unlike Python lists). On the other hand, they are
memory efficient and many operations run faster on them. They also allow
special manipulation, like reshaping and
broadcasting.
This lecture covers only the basic operations and concepts. Numpy offers many other functions, operations, tricks and convenience methods. You can find more information in the NumPy documentation.
Installation: pip install numpy
Import:
1.2 Array Creation
1.2.1 1D Arrays
1.2.1.1 From Python Lists:
1.2.1.2 Direct Creation with
np.r_:
The np.r_ function (notice, it’s actually not a
function) creates a 1D array from a list of values. It can also be used
to create more complex arrays (reader is encouraged to check the NumPy
documentation for more details).
1.2.1.3 Using Ranges and Linspace:
arange: [0 2 4 6 8]
arange with floats: [ 0.5 3. 5.5 8. 10.5]
linspace: [0. 0.25 0.5 0.75 1. ]
The function np.arange has the same signature as the
Python range function:
np.arange(start, stop, step). The np.linspace
function creates a sequence of evenly-spaced values, between start and
stop. The signature is
np.linspace(start, stop, num=100, endpoint=True), where
num is the number of samples to generate and
endpoint is a boolean that indicates whether to include the
stop value in the sequence (False for similar behavior to
range).
np.r_ can also be used to generate sequences, if instead
of a sequence of numbers, a slice is passed as an argument. This slice
has the same format as in the Python range function:
start:stop:step. However, unlike for range the
values do not have integers and they can even be imaginary
numbers, which makes this operation behave like
linspace.
values from 1 to 10, with step 2: [1 3 5 7 9]
values from 0 to 1, with step 0.1: [0. 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9]
5 values, evenly spaced between 0 and 1: [0. 0.25 0.5 0.75 1. ]
np.r_ can also be used to quickly combine multiple lists
and arrays into a 1D array - by default, it “flattens” all the input
arrays and lists.
combined array: [1 2 3 4 5 6 7 8 9]
You can even combine both the range creation and concatenation of lists and arrays:
1.2.1.4 Pre-filled Arrays:
NumPy supports several array pre-allocation methods.
1.2.1.5 Random Arrays:
5 random values in range [0, 1): [0.75826822 0.71297438 0.10664392 0.10559446 0.51753591]
5 random normally distributed values: [-1.43945581 -0.71147991 0.43317463 0.34501783 -1.31837289]
5 random integers in range [0, 10): [5 3 6 5 7]
Newest NumPy versions actually encourage the use of random generators
from the numpy.random module. However, for the purpose of
this course, we will not cover this in detail.
1.2.2 2D Arrays (Matrices)
1.2.2.1 From Nested Lists:
1.2.2.2 The
numpy.matrix Class
There is also a specific class for matrices in numpy -
numpy.matrix. It contains some matrix-specific syntax sugar
but it is also more “cumbersome” and thus normally not recommended
(unless you need nicer syntax and outputs).
One of the ‘advantages’ is that standard operations on matrix are automatically treated as matrix operations. For example, multiplication and division for arrays are elementwise, while in matrices they are matrix multiplication and matrix division.
Array multiplication:
[[ 1 4 9]
[16 25 36]
[49 64 81]]
Matrix multiplication:
[[ 30 36 42]
[ 66 81 96]
[102 126 150]]
Element-wise matrix multiplication:
[[ 1 4 9]
[16 25 36]
[49 64 81]]
1.2.2.3 Pre-filled 2D Arrays
Similarly to 1D arrays, you can create pre-filled and random 2D arrays:
# all matrices have 3 rows and 4 columns
zeros = np.zeros((3, 4))
ones = np.ones((3, 4))
sevens = np.full((3, 4), 7)
rand = np.random.rand(3,4)
randn = np.random.randn(3,4)
randint = np.random.randint(0, 10, (3, 4))
print("Zeros:\n", zeros)
print("Ones:\n", ones)
print("Sevens:\n", sevens)
print("Random:\n", rand)
print("Random normally distributed:\n", randn)
print("Random integers:\n", randint)Zeros:
[[0. 0. 0. 0.]
[0. 0. 0. 0.]
[0. 0. 0. 0.]]
Ones:
[[1. 1. 1. 1.]
[1. 1. 1. 1.]
[1. 1. 1. 1.]]
Sevens:
[[7 7 7 7]
[7 7 7 7]
[7 7 7 7]]
Random:
[[0.77894312 0.79034329 0.90613076 0.18308285]
[0.70067013 0.36286066 0.12750122 0.396946 ]
[0.74316564 0.96283611 0.35443555 0.84100978]]
Random normally distributed:
[[ 0.43569852 0.26171412 -1.39439897 -0.66912734]
[ 2.17821929 -0.13514886 0.94435442 -0.43713654]
[ 2.19567072 -0.40622 -1.26381399 -1.39303716]]
Random integers:
[[5 2 4 7]
[5 0 3 6]
[0 4 4 8]]
1.3 Array size
The default function len will only reveal the number of
rows of the array, not the number of elements (this is because of how
iterating over the arrays work). To get the total number of elements,
use the size attribute:
To see the sizes of the array along each dimension, you can use the
shape attribute:
Finally, to get the number of dimensions of the array, use the
ndim attribute:
1.3.0.1 Reshaping 1D Arrays
You can also take a 1D array and reshape it into a
2D array with the reshape array method. The reshape takes
the height and width (number of rows and columns) as arguments. And
example of use case is getting a 1D list of byte values and reshaping it
into a 2D array of pixel intensities for an image. This is how some
image formats store images. Reshape organizes the values row-by-row. You
have to make sure that there is enough values to fill the array, i.e.,
height * width == len(array_1d).
Reshaped array:
[[ 0 1 2 3]
[ 4 5 6 7]
[ 8 9 10 11]]
The following would throw an error, because there is not enough values to create a 2D array of the shape (4, 5):
You can also omit one of the dimensions when reshaping, in which case the other dimension will be calculated automatically. In this case, the length of the input (1D) array must be divisible by the specified value.
Reshaped array (3, -1):
[[ 0 1 2 3]
[ 4 5 6 7]
[ 8 9 10 11]]
Reshaped array (-1, 4):
[[ 0 1 2 3]
[ 4 5 6 7]
[ 8 9 10 11]]
The following would throw an error, because the length of the input array length is not divisible by 5:
Column-wise reshaped array:
[[ 0 3 6 9]
[ 1 4 7 10]
[ 2 5 8 11]]
...or using transposition 'a.T':
[[ 0 3 6 9]
[ 1 4 7 10]
[ 2 5 8 11]]
There are some performance nuances about contiguous arrays but that is beyond the scope of this course.
1.3.0.2 Indexing trick
np.r_
The np.r_ operator can be used to create a 2D array as
well. If all inputs are 2D, they can be directly concatenated. The
concatenation is row-wise by default.
a =
[[0 1 2]
[3 4 5]]
b =
[[ 6 7 8]
[ 9 10 11]]
Concatenated:
[[ 0 1 2]
[ 3 4 5]
[ 6 7 8]
[ 9 10 11]]
The inputs can be also 1D arrays or a mix of 1D and 2D arrays. In this case, a string directive is specified as the first “argument”. It determines how the arrays are concatenated. The format of the directive is as follows:
where: * axis - the axis along which
the arrays (remaining arguments) are concatenated *
min_dim is the minimum dimension. Inputs
with lower dimension are first “upgraded” to this dimension *
transpose_spec specifies how lower-dimensional inputs are
transposed before concatenation. It can be omitted, in which case it is
set to -1.
For example, a row-wise stacking into a 2D array would be specified as:
where a and b are 1D arrays. Note that the
concatenated arrays must have the same length.
For the axis, 0 means row-wise and 1 means column-wise
(can be higher for multi-dimensional arrays). For 2D arrays, the
min_dim must be set to 2. This means that all 1D inputs are
first “upgraded” to 2D. Already 2D inputs are not changed. The last
argument specifies “where to put” the upgraded 1D inputs. The default
value is -1, which means that for 2D case. the 1D inputs
are upgraded to have the dimension of [1, N], where N is the length of
the 1D input. If we change it to 0, the 1D inputs are
upgraded to have the dimension of [N, 1]. This happens before
concatenation and this it will affect how the result will be shaped.
For example, for K inputs, each being 1D with length N, the spec “0,2,-1” will result in a 2D array of shape (K, N): - row-wise stacking - upgrade 1D inputs to 2D with dimension [1, N] (the last “-1” means “put the 1D inputs into the last dimension”)
The spec “1,2,0” will result in a 2D array of shape (N, K): - column-wise stacking - upgrade 1D inputs to 2D with dimension [N, 1] (the last “0” means “put the 1D inputs into the first dimension”)
To make it perhaps more clear, for 3D case, the spec “0,3,-1” (the same as writing “0,3”) will upgrade 1D input of length N to a 3D array of shape [1, 1, N]. It will also upgrade 2D inputs of shape [M, N] to a 3D array of shape [1, M, N]. The spec “0,3,0” will upgrade 1D input of length N to a 3D array of shape [N, 1, 1]. It will also upgrade 2D inputs of shape [M, N] to a 3D array of shape [M, N, 1]. Lastly, the spec “0,3,1” will upgrade 1D input of length N to a 3D array of shape [1, N, 1]. For 2D inputs, it is the same as “0,3,-1”.
Additionally, you can also “append” 2D arrays or lists. Although, these must also have each row (or column) of the same length.
# Given our previous example:
c = np.r_[a, b]
print("Concatenated along axis '0':", c, c.shape, sep="\n")
c = np.r_["1", a, b]
print("Concatenated along axis '1':", c, c.shape, sep="\n")
c = np.r_["2,3", a, b]
print("Concatenated along axis '2':", c, c.shape, sep="\n")
c = np.r_["0,3", a, b]
print("Concatenated along axis '0' but *upgraded* to 3 dims:", c, c.shape, sep="\n")
print()
d, e = np.ones((1, 3)), np.zeros((3, ))
print("d shape:", d.shape)
print("e shape:", e.shape)
c = np.r_["1,2,1", d, e] # just "1,2" wouldn't work because zeros will have bad shape
print("Concatenation of arrays with different dims", c, c.shape, sep="\n")Concatenated along axis '0':
[[ 0 1 2]
[ 3 4 5]
[ 6 7 8]
[ 9 10 11]]
(4, 3)
Concatenated along axis '1':
[[ 0 1 2 6 7 8]
[ 3 4 5 9 10 11]]
(2, 6)
Concatenated along axis '2':
[[[ 0 1 2 6 7 8]
[ 3 4 5 9 10 11]]]
(1, 2, 6)
Concatenated along axis '0' but *upgraded* to 3 dims:
[[[ 0 1 2]
[ 3 4 5]]
[[ 6 7 8]
[ 9 10 11]]]
(2, 2, 3)
d shape: (1, 3)
e shape: (3,)
Concatenation of arrays with different dims
[[1. 1. 1. 0. 0. 0.]]
(1, 6)
Here, we combine multiple items into one 2D array.
a = np.array([1, 2, 3])
c = [7, 8, 9]
d = np.r_[
'0,2', # row-wise 2D array
a, # 1D array
4:7, # range(4, 7)
c, # list
[10, 11, 12], # list, directly specified
[[13, 14, 15], [16, 17, 18]], # 2D list
np.array([[19, 20, 21], [22, 23, 24]]), # 2D array
25:30:3j # range with 3 evenly spaced values
]
print("Row-wise stacking:\n", d)Row-wise stacking:
[[ 1. 2. 3. ]
[ 4. 5. 6. ]
[ 7. 8. 9. ]
[10. 11. 12. ]
[13. 14. 15. ]
[16. 17. 18. ]
[19. 20. 21. ]
[22. 23. 24. ]
[25. 27.5 30. ]]
You can also stack the items column-wise:
e = np.r_[
'1,2,0', # column-wise 2D array
a, # 1D array
4:7, # range(4, 7)
c, # list
[10, 11, 12], # list, directly specified
[[13, 14], [15, 16], [17, 18]], # 2D list
np.array([[19, 20], [21, 22], [23, 24]]), # 2D array
25:30:3j # range with 3 evenly spaced values
]
print("Column-wise stacking:\n", e)Column-wise stacking:
[[ 1. 4. 7. 10. 13. 14. 19. 20. 25. ]
[ 2. 5. 8. 11. 15. 16. 21. 22. 27.5]
[ 3. 6. 9. 12. 17. 18. 23. 24. 30. ]]
Note that the 2D arrays must have the same number of columns, in the column.wise stacking case.
More explanation of np.r_ can be found at https://rmoralesdelgado.com/all/numpy-concatenate-r_-c_/.
1.3.1 3D Arrays
All the previously shown methods for 1 an 2D arrays also work for 3D arrays. Actually, you can create an array of any dimension with similar techniques.
1.3.1.1 From Nested Lists:
3D Array:
[[[ 1 2 3]
[ 4 5 6]
[ 7 8 9]]
[[10 11 12]
[13 14 15]
[16 17 18]]]
1.3.1.2 From 1D arrays using reshape
Similarly to 2D, make sure that there is enough values to create a 3D array of the shape (height, width, depth). Also, you can omit one of the dimensions when reshaping, in which case the other dimension will be calculated automatically.
1.3.1.3 Pre-filled 3D Arrays
Each of these arrays has a shape of (2, 3, 4)
a = np.zeros((2, 3, 4))
b = np.ones((2, 3, 4))
c = np.full((2, 3, 4), 5)
d = np.random.rand(2, 3, 4)
e = np.random.randn(2, 3, 4)
f = np.random.randint(0, 10, (2, 3, 4))
print("Zeros:\n", a)
print("Ones:\n", b)
print("Fives:\n", c)
print("Random:\n", d)
print("Random normal:\n", e)
print("Random integers:\n", f)Zeros:
[[[0. 0. 0. 0.]
[0. 0. 0. 0.]
[0. 0. 0. 0.]]
[[0. 0. 0. 0.]
[0. 0. 0. 0.]
[0. 0. 0. 0.]]]
Ones:
[[[1. 1. 1. 1.]
[1. 1. 1. 1.]
[1. 1. 1. 1.]]
[[1. 1. 1. 1.]
[1. 1. 1. 1.]
[1. 1. 1. 1.]]]
Fives:
[[[5 5 5 5]
[5 5 5 5]
[5 5 5 5]]
[[5 5 5 5]
[5 5 5 5]
[5 5 5 5]]]
Random:
[[[0.75637257 0.77931249 0.19122926 0.28600979]
[0.02281568 0.43147339 0.10032653 0.96170334]
[0.02764253 0.75341728 0.5001757 0.18381847]]
[[0.74534629 0.15821779 0.08594429 0.43557004]
[0.54149154 0.7745313 0.36364269 0.97898759]
[0.60202276 0.17215901 0.72787529 0.45078846]]]
Random normal:
[[[-1.31034762 -1.88587613 1.1599378 0.76065276]
[-1.04898525 0.33406851 -0.33750265 -0.12861431]
[-0.5595889 0.34307806 -1.31848681 0.21285843]]
[[ 1.46368541 -1.74345057 0.33803027 0.12079975]
[ 0.82734626 0.79245022 0.10998902 0.2026285 ]
[ 0.94643234 -0.00701968 -1.43167701 -1.04515155]]]
Random integers:
[[[1 2 5 4]
[9 7 7 4]
[3 0 5 0]]
[[6 3 2 9]
[7 0 3 9]
[1 7 0 6]]]
1.3.1.4 Using
np.r_
Similarly to previous cases, you can use np.r_ to create
3D arrays.
1.4 Data type
By default, Numpy automatically determines the “best” data
type for a given array, based on the provided values. E.g., if
all the values are integers, it will be an integer array. If any of the
values is a float, it will be a float array. Strings and chars are also
supported. You can also create mixed arrays, in which case the data type
will be object (though, this is not recommended).
Often, however, you will need to specify the data type explicitly. There are three main reasons for this:
- You want to avoid automatic type conversion.
- You want to control the memory usage (64-bit ints and floats are used by default).
- Specific data type is required for your specific application (e.g., if you need to store binary or image data).
Automatic data type conversion might happen, for example, when you append a numeric value to a string array.
String array: ['a' 'b' 'c' 'd' '5.0' '7']
Data type: <U32
Notice that both 5.0 and 7 where converted
to strings. U32 means fixed-length unicode string of 32
characters - that is 32x4 bytes for each item. Numpy arrays store string
data in strings of equal length - the length is determined and expanded
automatically based on the maximum length of the string in the
array.
The memory issue comes into place when working with very large arrays, for example, images or large data sets.
First, create a helper function to print the data type and memory usage:
The sys.getsizeof function returns the size in bytes
that the object takes up in the memory.
Let’s create a matrix with three million elements (1000x1000x3) of byte values (0-255, e.g., typical image).
Memory usage for data type 'int64': 22.888 MB
Memory usage for data type 'uint8': 2.861 MB
True
The default string data type - unicode - is also less memory efficient - 4 bytes are needed for each character. It might be useful to change if you need to store large text data with only ASCII characters.
import string # import the string module and get the list of ASCII letters
letters = np.array(list(string.ascii_letters))
random_text = np.random.choice(letters, int(1e6)) # 1 million characters
print_memory_usage(random_text)
random_bytes = random_text.astype(np.bytes_) # 1-byte characters
print_memory_usage(random_bytes)
random_bytes = random_text.astype('U32') # Unicode, up to 32 chars
print_memory_usage(random_bytes)Memory usage for data type '<U1': 3.815 MB
Memory usage for data type '|S1': 0.954 MB
Memory usage for data type '<U32': 122.070 MB
1.4.0.1 Setting the data type
You can set data type manually using the dtype
parameter, when creating the array.
Memory usage for data type 'int16': 0.000 MB
Some other Numpy functions also support the dtype
parameter. For example, np.random.randint:
1.4.0.2 Converting data types
Existing arrays can also be converted to different data types with
the astype array method. For example:
1.4.0.3 Defining the data type
You have already seen a few data types. Basic data types are simply
provided by their Python type name (e.g., int,
float, str, bool). For more
specific types, such as integer with specific number of bits, you can
use the types defined by Numpy (e.g., np.int32,
np.float64). You can also specify a data type using
np.dtype function. It takes the data type code as an
argument (e.g., np.dtype('int32')). You have seen some of
these codes when we were printing the size of arrays of different
types.
For reference, here is a list of base codes:
Character Code:
- i: Signed integer.
- u: Unsigned integer.
- f: Float.
- c: Complex.
- b: Boolean.
- S: Bytes (ASCII string).
- U: Unicode string.
- M: Datetime.
- m: Timedelta.
- O: Python object.
- V: Void (raw data).
Of these, you will most likely only use ‘i’, ‘u’, ‘f’ and maybe ‘U’,
which can be defined directly: np.int<bits>,
np.uint<bits>, np.float<bits>,
np.str_ (substitute <bits> with the
number of bits used by the specific data type).
Note that for direct type definition (using the single
letter code) you don’t specify the number of bits, but
bytes. For example, np.int32 is “i4” (32
bits is 4 bytes) and np.float64 is “f8” (64 bits is 8
bytes).
Nonetheless, it is useful to know the other types in case your array
“get converted to” one of those and you will see that code when you
inspect the dtype attribute of the array.
For completeness, the code might also include <,
>, |. This indicates the bit ordering
(little or big endian).
1.5 Indexing
Indexing works in similar way to the Python lists, i.e., the “usual” indexing and slicing. The difference is that for multi-dimensional lists the indexing is:
while for multi-dimensional Numpy arrays the indexing is:
That is, packed into single square brackets.
1.5.1 Simple indexing
a = np.array([1, 2, 3, 4, 5])
print("Element at index 0:", a[0])
print("Element at index -1:", a[-1])
b = np.array([[1, 2, 3], [4, 5, 6]])
print("Element at row 1, col 2:", b[1, 2])
print("Element at row -1, col 0:", b[-1, 0])
print("Element at row -1 (last row), col -2 (second to last):", b[-1, -2])
c = np.random.rand(3, 4, 5)
print("Element at row 1, col 2, depth 3:", c[1, 2, 3])
print("Element at row 0, col 0, depth 0:", c[0, 0, 0])
print("Element at row -1, col -1, depth -1:", c[-1, -1, -1])Element at index 0: 1
Element at index -1: 5
Element at row 1, col 2: 6
Element at row -1, col 0: 4
Element at row -1 (last row), col -2 (second to last): 5
Element at row 1, col 2, depth 3: 0.7627047716005501
Element at row 0, col 0, depth 0: 0.6133815526246021
Element at row -1, col -1, depth -1: 0.4623539634602747
1.5.2 Slicing
One bid advantage of Numpy arrays is that you can create multi-dimensional slices of multi-dimensional arrays. For example:
First row of m:
[[0.93197096 0.72451667 0.0771316 0.6144562 0.66842905]
[0.7323445 0.43760015 0.72353614 0.92065962 0.0994233 ]
[0.85596307 0.22353252 0.26398603 0.71328628 0.88421908]
[0.26573301 0.71864285 0.98959795 0.26903377 0.32674218]]
First column of m:
[[0.93197096 0.72451667 0.0771316 0.6144562 0.66842905]
[0.36294568 0.48395834 0.81476111 0.71882874 0.3979581 ]
[0.62805727 0.85009981 0.98305393 0.31145772 0.15275628]]
First depth slice of m:
[[0.93197096 0.7323445 0.85596307 0.26573301]
[0.36294568 0.43799161 0.05912082 0.57648699]
[0.62805727 0.13879755 0.60034962 0.47985244]]
All of these are 2D matrices - slices from a 3D array.
The following will create a 3D slice of the matrix
m:
[[[0.6144562 0.66842905]
[0.92065962 0.0994233 ]]
[[0.71882874 0.3979581 ]
[0.09818541 0.99601839]]
[[0.31145772 0.15275628]
[0.43968882 0.37657103]]]
In this case, we took all of the rows (indicated by :),
the first two columns (:2), and the last two depth slices
(-2:).
If this seems a little confusing - each element in the brackets (separated by comma), specifies slice for the specific dimension. Otherwise, the slicing principle is the same as for Python lists (i.e., [start, stop, step], with each argument being optional).
The only small difference is that you can provide colon
: for each dimension, which will indicate “use all of this
dimension”. E.g., a[:, 2] means “from each row, take the
element from the column 2” (third column, since its 0-based index). Or
in other words, create a slice of the whole second column. For more
dimensional arrays, you can also use ellipsis ..., which
will count as putting colon : for each of the corresponding
dimensions. For example, for a 3D array, you can either specify
a[:, :, 3], which means “take the third depth slice (last
dimension) for each row and column (first two dimensions)”. An
equivalent form would be a[..., 3], which does exactly the
same.
Lastly, omitting dimension specification at the end is the same as
using colon or ellipsis for them. E.g., for a 3D array, this
a[0, :, :] will take each column and depth slice for the
first row of the 3D array (thus, creating a 2D sub-matrix located at row
0 in the original matrix). This is the same as a[0, ...] or
a[0].
1.5.3 Assignment
The assignment works in the same way as in Python lists. However, you can directly assign to multi-dimensional slices of multi-dimensional arrays. What’s more, you can either assign scalars (each value of the slice will be set to this value) or multi-dimensional arrays (of the same dimension as the slice).
Scalar assignment to a slice along a single dimension:
[[[ 0 1 2 3]
[ 4 5 6 7]
[ 8 9 10 11]]
[[12 13 14 15]
[16 17 18 19]
[20 21 22 23]]]
---
[[[ 1 1 1 1]
[ 1 1 1 1]
[ 1 1 1 1]]
[[12 13 14 15]
[16 17 18 19]
[20 21 22 23]]]
---
[[[ 2 2 2 2]
[ 1 1 1 1]
[ 1 1 1 1]]
[[ 2 2 2 2]
[16 17 18 19]
[20 21 22 23]]]
---
[[[ 3 2 2 2]
[ 3 1 1 1]
[ 3 1 1 1]]
[[ 3 2 2 2]
[ 3 17 18 19]
[ 3 21 22 23]]]
---
Scalar assignment to a 2D slice:
[[[ 0 1 4 4]
[ 4 5 4 4]
[ 8 9 10 11]]
[[12 13 4 4]
[16 17 4 4]
[20 21 22 23]]]
Assigning a 2D array to a 2D slice:
b:
[[0.43596121 0.86217116]
[0.21254856 0.13158863]]
---
a (modif):
[[[ 0. 1. 0.436 0.8623]
[ 4. 5. 0.2125 0.1316]
[ 8. 9. 10. 11. ]]
[[12. 13. 14. 15. ]
[16. 17. 18. 19. ]
[20. 21. 22. 23. ]]]
Assigning a 3D array to a 3D slice:
b:
[[[0.17682628 0.93094934]
[0.29890664 0.37152371]]
[[0.36369581 0.18450318]
[0.66012069 0.58281168]]]
---
a (modif):
[[[ 0. 1. 0.1769 0.931 ]
[ 4. 5. 0.2988 0.3716]
[ 8. 9. 10. 11. ]]
[[12. 13. 0.3638 0.1844]
[16. 17. 0.66 0.583 ]
[20. 21. 22. 23. ]]]
Assigning a 1D array to a 3D slice:
b:
[0.52007453 0.06204009]
---
a (modif):
[[[ 0. 1. 0.52 0.06204]
[ 4. 5. 0.52 0.06204]
[ 8. 9. 10. 11. ]]
[[12. 13. 0.52 0.06204]
[16. 17. 0.52 0.06204]
[20. 21. 22. 23. ]]]
The cases where we assign a lower-dimensional array to a higher-dimensional slice showcases the broadcasting ability of Numpy arrays. See below for more details.
1.5.4 (Re)shaping arrays
1.5.4.1 Reshape
As you’ve seen, the reshape method allows you to change
the shape of the array. There, you simply specify the desired dimensions
of the new array. One thing to keep in mind is that by default, this
creates a view into the original array, not a copy. You can, however,
specify that you want a copy.
[[[ 0 1 2 3]
[ 4 5 6 7]
[ 8 9 10 11]]
[[12 13 14 15]
[16 17 18 19]
[20 21 22 23]]]
[[ 0 1 2 3 4 5 6 7]
[ 8 9 10 11 12 13 14 15]
[16 17 18 19 20 21 22 23]]
a:
[[[100 1 2 3]
[ 4 5 6 7]
[ 8 9 10 11]]
[[ 12 13 14 15]
[ 16 17 18 19]
[ 20 21 22 23]]]
a:
[[[100 1 2 3]
[ 4 5 6 7]
[ 8 9 10 11]]
[[ 12 13 14 15]
[ 16 17 18 19]
[ 20 21 22 23]]]
Note that modifying b does change the original array
a, while modifying c does not.
1.5.4.2 Increasing dimensions
Sometimes, you might need to increase the dimensionality of an array
by creating singleton dimensions (one-element
dimension). This can be done either by reshape or
inserting new axis into the index. Typical usage for
this is if we create a 1D vector, e.g., using arange, and
want to convert it to a 2D array.
For the reshape approach, we simply specify 1 for the
new dimension:
[0 1 2 3 4 5]
[[0]
[1]
[2]
[3]
[4]
[5]]
[[0 1 2 3 4 5]]
Notice that b is a 2D vector with 6 rows and 1 column
and c is a 2D vector with 1 row and 6 columns.
You can also supply -1 for the non-singleton dimension,
to make things simpler and more general. And you can create more than 2D
arrays.
1.5.4.3 New axis
Alternatively, you can use np.newaxis with indexing
trick. The following will yield the same result as using
reshape:
1.5.4.4 Squeezing out singleton dimensions
Sometimes, you need to do the inverse, i.e., remove singleton
dimensions, Numpy offers the squeeze method:
b
[[0 1 2 3 4 5]] (1, 6)
b squeezed
[0 1 2 3 4 5] (6,)
c
[[0]
[1]
[2]
[3]
[4]
[5]] (6, 1)
c squeezed
[0 1 2 3 4 5] (6,)
1.6 Iteration over arrays
Even though vectorized operations are preferred (see next section),
sometimes you may want to use a loop with arrays (e.g., for debugging or
custom operations). Like the Python lists, you can use for
loop to iterate over the indices or the for each
version. In the later case, the iteration will happen along the
first dimension of the array (row).
1.6.1 Iterating over 1D arrays
For 1D arrays, the iteration is fairly simple:
1.6.2 Iterating over 2D arrays
For higher dimensional arrays, you can iterate over the rows. For example:
[0 1 2 3]
[4 5 6 7]
[ 8 9 10 11]
Subsequent iteration will iterate over the first dimension of the “slice”:
1.6.3 Iterating over 3D arrays
1.6.4 Linear iteration over multi-dimensional arrays
You can directly iterate over individual elements of a
multi-dimensional array (called linear iteration). You can either
ravel the array or use the nditer
iterator.
The ravel method will simply “stretch” or flatten the
array into a 1D array, row-by-row. It has (almost) the same effect as
reshape(-1), although, it is the preferred method. Both of
these two methods will return a flattened view of the
array (not a copy). The difference is that ravel returns a
contiguous view, if possible. This means, certain
operations will be faster. There is also a flatten method,
which also returns a 1D array, but it returns a copy of
the original array instead of a view.
Original array:
[[[ 0 1 2 3]
[ 4 5 6 7]
[ 8 9 10 11]]
[[12 13 14 15]
[16 17 18 19]
[20 21 22 23]]]
Flattened (raveled) array:
[ 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23]
Try uncommenting any one of the “flattening” lines to see the difference:
Original array:
[[[ 0 2 4 6]
[ 8 10 12 14]
[16 18 20 22]]
[[24 26 28 30]
[32 34 36 38]
[40 42 44 46]]]
Modified flattened array:
[ 0 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38 40 42 44 46]
Linear iteration with np.nditer. This function returns a
special iterator object that can be used by the for
loop. It is actually a very powerful method that allows all sorts of
special iterations but that is beyond the scope of this course. You can
read more about it here.
1.7 Arithmetic operations and vectorization
A big advantage of Numpy arrays is that you can perform arithmetic operations on them directly. Unlike Python lists, for which we had to implement functions, iterating over individual elements, you can compute certain operations directly on the array.
Here, we go through some basic functions. A more complete list of mathematical functions that Numpy provides can be found in its documentation.
1.7.1 Basic arithmetic operations
1.7.2 Mathematical functions
The Numpy library also specifies a handful of mathematical operations:
square root of A:
[[0. 1. 1.41421356 1.73205081]
[2. 2.23606798 2.44948974 2.64575131]
[2.82842712 3. 3.16227766 3.31662479]]
exponential of A:
[[1.00000000e+00 2.71828183e+00 7.38905610e+00 2.00855369e+01]
[5.45981500e+01 1.48413159e+02 4.03428793e+02 1.09663316e+03]
[2.98095799e+03 8.10308393e+03 2.20264658e+04 5.98741417e+04]]
logarithm of A:
[[-2.07232658e+01 1.00000008e-09 6.93147181e-01 1.09861229e+00]
[ 1.38629436e+00 1.60943791e+00 1.79175947e+00 1.94591015e+00]
[ 2.07944154e+00 2.19722458e+00 2.30258509e+00 2.39789527e+00]]
sine of A:
[[ 0. 0.84147098 0.90929743 0.14112001]
[-0.7568025 -0.95892427 -0.2794155 0.6569866 ]
[ 0.98935825 0.41211849 -0.54402111 -0.99999021]]
cosine of A:
[[ 1. 0.54030231 -0.41614684 -0.9899925 ]
[-0.65364362 0.28366219 0.96017029 0.75390225]
[-0.14550003 -0.91113026 -0.83907153 0.0044257 ]]
1.7.3 Aggregation functions
A = np.arange(12).reshape(3, 4)
print("A:\n", A)
print()
print(f"sum of A:\n{np.sum(A)}")
print(f"mean of A:\n{np.mean(A)}")
print(f"minimum of A:\n{np.min(A)}")
print(f"maximum of A:\n{np.max(A)}")
print(f"standard deviation of A:\n{np.std(A)}")
print(f"variance of A:\n{np.var(A)}")
print(f"median of A:\n{np.median(A)}")
print(f"cumulative sum of A:\n{np.cumsum(A)}")
print(f"cumulative product of A:\n{np.cumprod(A)}")
print(f"norm of A:\n{np.linalg.norm(A)}")A:
[[ 0 1 2 3]
[ 4 5 6 7]
[ 8 9 10 11]]
sum of A:
66
mean of A:
5.5
minimum of A:
0
maximum of A:
11
standard deviation of A:
3.452052529534663
variance of A:
11.916666666666666
median of A:
5.5
cumulative sum of A:
[ 0 1 3 6 10 15 21 28 36 45 55 66]
cumulative product of A:
[0 0 0 0 0 0 0 0 0 0 0 0]
norm of A:
22.494443758403985
For the aggregation functions, the dimension along which the aggregation should be done, can be specified. Some of the aggregation functions are also array methods. For example:
print(f"sum of A along axis 0 (row-wise):\n{A.sum(axis=0)}")
print(f"sum of A along axis 1 (column-wise):\n{A.sum(axis=1)}")
print(f"mean of A along axis 0:\n{A.mean(axis=0)}")
print(f"mean of A along axis 1:\n{A.mean(axis=1)}")
print(f"norm of A along axis 0:\n{np.linalg.norm(A, axis=0)}")
print(f"norm of A along axis 1:\n{np.linalg.norm(A, axis=1)}")sum of A along axis 0 (row-wise):
[12 15 18 21]
sum of A along axis 1 (column-wise):
[ 6 22 38]
mean of A along axis 0:
[4. 5. 6. 7.]
mean of A along axis 1:
[1.5 5.5 9.5]
norm of A along axis 0:
[ 8.94427191 10.34408043 11.83215957 13.37908816]
norm of A along axis 1:
[ 3.74165739 11.22497216 19.13112647]
1.7.4 Operations between arrays
1.7.4.1 Mathematical operations between arrays
Much like arithmetics with scalars, many operations can be performed between arrays.
All of the following operations are element-wise:
a + b =
[[12 14 16 18]
[20 22 24 26]
[28 30 32 34]]
a - b =
[[-12 -12 -12 -12]
[-12 -12 -12 -12]
[-12 -12 -12 -12]]
a * b =
[[ 0 13 28 45]
[ 64 85 108 133]
[160 189 220 253]]
a / b =
[[0. 0.07692308 0.14285714 0.2 ]
[0.25 0.29411765 0.33333333 0.36842105]
[0.4 0.42857143 0.45454545 0.47826087]]
a ** b =
[[ 0 1 16384
14348907]
[ 4294967296 762939453125 101559956668416
11398895185373143]
[ 1152921504606846976 -1261475310744950487 1864712049423024128
6839173302027254275]]
There are also matrix versions of sum of the operations, for example, the matrix dot product (for vectors, it will be the inner product):
a . b =
[[114 120 126]
[378 400 422]
[642 680 718]]
Alternatively, as mentioned before, for 2D arrays, you can convert
them directly into the matrix class and all operations on
them will be matrix operations (where applicable).
1.7.4.2 Broadcasting
Another big advantage of Numpy is the broadcasting ability. Broadcasting means expanding a lower-dimensional array to allow arithmetic operation with higher-dimensional arrays. In the simplest case, you can see this when multiplying a scalar with a 1 or higher dimensional array. The scalar is “repeated” over all elements of the array, and the result is a new array with the same shape as the original array.
It is possible to do this with even higher dimensional inputs, e.g.,
1D vs 2D or 2D vs 3D arrays. This ability have, of course, certain
limitations. It must be possible to do the broadcasting in a meaningful,
unambiguous way. For example, you can multiply a column vector of shape
[1, M] with a matrix of shape [N, M]. In this case, the vector is
“copied” over each row. However, it would not make sense to do the same
with a vector of shape [1, K], where K != M
1D vs 2D broadcasting
N, M = 3, 4
a = np.arange(N * M).reshape(N, M)
b = np.arange(M).reshape(1, M)
c = np.arange(N).reshape(N, 1)
d = np.arange(M) # this also works but has limited use
print(a.shape, b.shape)
print(a * b)
print(a.shape, c.shape)
print(a * c)
print(a.shape, d.shape)
print(d * a) # this is always row-wise(3, 4) (1, 4)
[[ 0 1 4 9]
[ 0 5 12 21]
[ 0 9 20 33]]
(3, 4) (3, 1)
[[ 0 0 0 0]
[ 4 5 6 7]
[16 18 20 22]]
(3, 4) (4,)
[[ 0 1 4 9]
[ 0 5 12 21]
[ 0 9 20 33]]
2D vs 3D broadcasting
1.8 Array filtering / masking (boolean indexing)
Numpy also offers boolean indexing. This can be created, for example, by comparing (or applying any other operation with a boolean result) arrays with a scalar or another array.
A:
[[ 0 1 2 3]
[ 4 5 6 7]
[ 8 9 10 11]]
A > 5:
[[False False False False]
[False False True True]
[ True True True True]]
A[A > 5]:
[ 6 7 8 9 10 11]
A[A % 2 == 0]:
[ 0 2 4 6 8 10]
A[A % 2 == 1]:
[ 1 3 5 7 9 11]
1.8.1
where function
As an alternative, useful in more complex situations, there is the
where function. It can be used to create a “flat” index
from a boolean condition. It can also be used to create a new array
directly.
In the later case, the where function is called with the
following arguments:
If the condition is True, the true_values
will be added to the new array. Otherwise, the false_values
will be added to the new array.
A:
[[ 0 1 2 3]
[ 4 5 6 7]
[ 8 9 10 11]]
A where A > 5:
(array([1, 1, 2, 2, 2, 2]), array([2, 3, 0, 1, 2, 3]))
A where A > 5:
[[ 0 0 0 0]
[ 0 0 6 7]
[ 8 9 10 11]]
A where A % 2 == 0:
[[ 0 0 2 0]
[ 4 0 6 0]
[ 8 0 10 0]]
A where A % 2 == 1:
[[ 0 1 0 3]
[ 0 5 0 7]
[ 0 9 0 11]]
1.9 Array concatenation
There are a few functions to combine existing arrays.
1.9.0.1 Concatenating arrays
Concatenated:
[1 2 3 4 5 6]
Concatenated:
[[1]
[2]
[3]
[4]
[5]
[6]]
Concatenated:
[[1 4]
[2 5]
[3 6]]
1.9.0.2 stacking Arrays Vertically and Horizontally
Vertical stack:
[[1 2]
[3 4]
[5 6]]
Horizontal stack:
[[1 2 5]
[3 4 6]]
There is also a dstack function which stacks arrays in
the third dimension.
1.9.0.3 Splitting and stacking arrays
You can also split arrays
[ 0 1 2 3 4 5 6 7 8 9 10 11]
[array([0, 1, 2, 3]), array([4, 5, 6, 7]), array([ 8, 9, 10, 11])]
[[ 0 1 2 3]
[ 4 5 6 7]
[ 8 9 10 11]]
Split 0: [0 1 2 3]
Split 1: [4 5 6 7]
Split 2: [ 8 9 10 11]
1.10 Data saving and loading
NumPy offers several methods for persisting arrays to disk and reading them back.
1.10.1 Saving and Loading in Binary Format (.npy/.npz)
1.10.1.1 Using np.save and np.load
1.10.1.2 Saving Multiple Arrays with np.savez:
x: [0 1 2 3 4 5 6 7 8 9]
y: [0. 0.11111111 0.22222222 0.33333333 0.44444444 0.55555556
0.66666667 0.77777778 0.88888889 1. ]
1.10.2 Saving and Loading as Text Files
Using np.savetxt and np.loadtxt: