Open In Colab

The Basics of NumPy Arrays

Data manipulation in Python is nearly synonymous with NumPy array manipulation: even newer tools like Pandas are built around the NumPy array. This section will present several examples of using NumPy array manipulation to access data and subarrays, and to split, reshape, and join the arrays. While the types of operations shown here may seem a bit dry and pedantic, they comprise the building blocks of many other examples used throughout the book. Get to know them well!

We'll cover a few categories of basic array manipulations here:

  • Attributes of arrays: Determining the size, shape, memory consumption, and data types of arrays
  • Indexing of arrays: Getting and setting the value of individual array elements
  • Slicing of arrays: Getting and setting smaller subarrays within a larger array
  • Reshaping of arrays: Changing the shape of a given array
  • Joining and splitting of arrays: Combining multiple arrays into one, and splitting one array into many

Before we do so, let's have another look at the many ways of creating arrays.

Creating Arrays

A Numpy Array is an iterable data type which is in essence very similar to a list. However, elements within an array must be of the same type!

Du to the many possible applications of arrays, they are one of the most used objects in the numpy library.

We can create an array by defining a list and passing this list into numpy's array function.

In [ ]:
import numpy as np
In [ ]:
mylist = [1, 2, 3]
x = np.array(mylist)
x
Out[ ]:
array([1, 2, 3])

We can also directly pass a list into the array function

In [ ]:
y = np.array([4, 5, 6])
y
Out[ ]:
array([4, 5, 6])

Numpy can also create multidimensional arrays. They correspond to nested lists and are often used to represent rows and columns in a table.

In [ ]:
multi_a = np.array([[7, 8, 9], [10, 11, 12]])
multi_a
Out[ ]:
array([[ 7,  8,  9],
       [10, 11, 12]])

We can find the number of rows and columns of a (multidimensional) array using the shape operator. The return will be (# rows, # columns)

In [ ]:
multi_a.shape
# our array has two rows and three columns
Out[ ]:
(2, 3)

In case we do not want to type in every single value of our array, we can use the arrange method to create an array with specific attributes.

np.arrange(start:end:step)

  • The start parameter indicates the starting value of our array. This defaults to 0.
  • The end parameter tells us before which value the array should stop
  • The step parameter defines an intervall within the individual elements.
In [ ]:
n = np.arange(0, 30, 2) # start at 0 count up by 2, stop before 30
n
Out[ ]:
array([ 0,  2,  4,  6,  8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28])

One further option to arrange the content of our array is linspace Simply said, linspace returns evenly spaced numbers over a specified interval.

While this sounds a lot like np.arrange, it's syntax is quite different:

np.linspace(start:end:num)

  • start indicates the starting value.
  • End indicates the end value.
  • The num parameter indicates the amount of evenly spaced values you want between start and end. It defaults to 50.
In [ ]:
o = np.linspace(0, 4, 9) # return 9 evenly spaced values from 0 to 4
o
Out[ ]:
array([0. , 0.5, 1. , 1.5, 2. , 2.5, 3. , 3.5, 4. ])
In [ ]:
o = np.linspace(0, 4) # return 50 evenly spaced values from 0 to 4
o

If we want to work with several arrays, we must ensure that they have the same amount of rows and columns.

Let's change the shape of our array using the reshape method. Again you can specify the intended number of rows and columns of the desired multidimensional array. Be aware, however, that the shape of the multidimensional array must match the number of elements within the array.

In [ ]:
n = n.reshape(3, 5) # reshape array to be 3x5
n
Out[ ]:
array([[ 0,  2,  4,  6,  8],
       [10, 12, 14, 16, 18],
       [20, 22, 24, 26, 28]])

If we want to reshape our array "in-place", i.e. without assigning it to a variable first, we can use resize instead of reshape. Resize thus does not return a value, but exercises the changes directly.

In [ ]:
n.resize(5,3 )
n
Out[ ]:
array([[ 0,  2,  4],
       [ 6,  8, 10],
       [12, 14, 16],
       [18, 20, 22],
       [24, 26, 28]])

Numpy also includes the option to create an array with a specific shape and fill this array with either ones (np.ones) or zeros (np.zeros).

In [ ]:
np.ones((3, 2)) #returns a new arre with given shape, filled with ones
Out[ ]:
array([[1., 1.],
       [1., 1.],
       [1., 1.]])
In [ ]:
np.zeros((2, 3)) # returns a new array of given shape and type, filled with zeros.
Out[ ]:
array([[0., 0., 0.],
       [0., 0., 0.]])

eye returns a 2-D array with ones on the diagonal and zeros elsewhere.

In [ ]:
np.eye(3)
Out[ ]:
array([[1., 0., 0.],
       [0., 1., 0.],
       [0., 0., 1.]])

diag extracts a diagonal or constructs a diagonal array.

In [ ]:
diag_ = np.array([4, 5, 6])
print(diag_)
np.diag(diag_)
[4 5 6]
Out[ ]:
array([[4, 0, 0],
       [0, 5, 0],
       [0, 0, 6]])

NumPy Array Attributes

First let's discuss some useful array attributes. We'll start by defining three random arrays, a one-dimensional, two-dimensional, and three-dimensional array. We'll use NumPy's random number generator, which we will seed with a set value in order to ensure that the same random arrays are generated each time this code is run:

In [ ]:
import numpy as np
np.random.seed(0)  # seed for reproducibility

x1 = np.random.randint(10, size=6)  # One-dimensional array
x2 = np.random.randint(10, size=(3, 4))  # Two-dimensional array
x3 = np.random.randint(10, size=(3, 4, 5))  # Three-dimensional array

Each array has attributes ndim (the number of dimensions), shape (the size of each dimension), and size (the total size of the array):

In [ ]:
print("x3 ndim: ", x3.ndim)
print("x3 shape:", x3.shape)
print("x3 size: ", x3.size)
x3 ndim:  3
x3 shape: (3, 4, 5)
x3 size:  60

Another useful attribute is the dtype, the data type of the array (which we discussed previously in Understanding Data Types in Python):

In [ ]:
print("dtype:", x3.dtype)
dtype: int64

Other attributes include itemsize, which lists the size (in bytes) of each array element, and nbytes, which lists the total size (in bytes) of the array:

In [ ]:
print("itemsize:", x3.itemsize, "bytes")
print("nbytes:", x3.nbytes, "bytes")
itemsize: 8 bytes
nbytes: 480 bytes

In general, we expect that nbytes is equal to itemsize times size.

Array Indexing: Accessing Single Elements

If you are familiar with Python's standard list indexing, indexing in NumPy will feel quite familiar. In a one-dimensional array, the $i^{th}$ value (counting from zero) can be accessed by specifying the desired index in square brackets, just as with Python lists:

In [ ]:
x1
Out[ ]:
array([5, 0, 3, 3, 7, 9])
In [ ]:
x1[0]
Out[ ]:
5
In [ ]:
x1[4]
Out[ ]:
7

To index from the end of the array, you can use negative indices:

In [ ]:
x1[-1]
Out[ ]:
9
In [ ]:
x1[-2]
Out[ ]:
7

In a multi-dimensional array, items can be accessed using a comma-separated tuple of indices:

In [ ]:
x2
Out[ ]:
array([[3, 5, 2, 4],
       [7, 6, 8, 8],
       [1, 6, 7, 7]])
In [ ]:
x2[0, 0]
Out[ ]:
3
In [ ]:
x2[2, 0]
Out[ ]:
1
In [ ]:
x2[2, -1]
Out[ ]:
7

Values can also be modified using any of the above index notation:

In [ ]:
x2[0, 0] = 12
x2
Out[ ]:
array([[12,  5,  2,  4],
       [ 7,  6,  8,  8],
       [ 1,  6,  7,  7]])

Keep in mind that, unlike Python lists, NumPy arrays have a fixed type. This means, for example, that if you attempt to insert a floating-point value to an integer array, the value will be silently truncated. Don't be caught unaware by this behavior!

In [ ]:
x1[0] = 3.14159  # this will be truncated!
x1
Out[ ]:
array([3, 0, 3, 3, 7, 9])

Array Slicing: Accessing Subarrays

Just as we can use square brackets to access individual array elements, we can also use them to access subarrays with the slice notation, marked by the colon (:) character. The NumPy slicing syntax follows that of the standard Python list; to access a slice of an array x, use this:

x[start:stop:step]

If any of these are unspecified, they default to the values start=0, stop=size of dimension, step=1. We'll take a look at accessing sub-arrays in one dimension and in multiple dimensions.

One-dimensional subarrays

In [ ]:
x = np.arange(10)
x
Out[ ]:
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
In [ ]:
x[:5]  # first five elements
Out[ ]:
array([0, 1, 2, 3, 4])
In [ ]:
x[5:]  # elements after index 5
Out[ ]:
array([5, 6, 7, 8, 9])
In [ ]:
x[4:7]  # middle sub-array
Out[ ]:
array([4, 5, 6])
In [ ]:
x[::2]  # every other element
Out[ ]:
array([0, 2, 4, 6, 8])
In [ ]:
x[1::2]  # every other element, starting at index 1
Out[ ]:
array([1, 3, 5, 7, 9])

A potentially confusing case is when the step value is negative. In this case, the defaults for start and stop are swapped. This becomes a convenient way to reverse an array:

In [ ]:
x[::-1]  # all elements, reversed
Out[ ]:
array([9, 8, 7, 6, 5, 4, 3, 2, 1, 0])
In [ ]:
x[5::-2]  # reversed every other from index 5
Out[ ]:
array([5, 3, 1])

Multi-dimensional subarrays

Multi-dimensional slices work in the same way, with multiple slices separated by commas. For example:

In [ ]:
x2
Out[ ]:
array([[12,  5,  2,  4],
       [ 7,  6,  8,  8],
       [ 1,  6,  7,  7]])
In [ ]:
x2[:2, :3]  # two rows, three columns
Out[ ]:
array([[12,  5,  2],
       [ 7,  6,  8]])
In [ ]:
x2[:3, ::2]  # all rows, every other column
Out[ ]:
array([[12,  2],
       [ 7,  8],
       [ 1,  7]])

Finally, subarray dimensions can even be reversed together:

In [ ]:
x2[::-1, ::-1]
Out[ ]:
array([[ 7,  7,  6,  1],
       [ 8,  8,  6,  7],
       [ 4,  2,  5, 12]])

Accessing array rows and columns

One commonly needed routine is accessing of single rows or columns of an array. This can be done by combining indexing and slicing, using an empty slice marked by a single colon (:):

In [ ]:
print(x2[:, 0])  # first column of x2
[12  7  1]
In [ ]:
print(x2[0, :])  # first row of x2
[12  5  2  4]

In the case of row access, the empty slice can be omitted for a more compact syntax:

In [ ]:
print(x2[0])  # equivalent to x2[0, :]
[12  5  2  4]

Subarrays as no-copy views

One important–and extremely useful–thing to know about array slices is that they return views rather than copies of the array data. This is one area in which NumPy array slicing differs from Python list slicing: in lists, slices will be copies. Consider our two-dimensional array from before:

In [ ]:
print(x2)
[[12  5  2  4]
 [ 7  6  8  8]
 [ 1  6  7  7]]

Let's extract a $2 \times 2$ subarray from this:

In [ ]:
x2_sub = x2[:2, :2]
print(x2_sub)
[[12  5]
 [ 7  6]]

Now if we modify this subarray, we'll see that the original array is changed! Observe:

In [ ]:
x2_sub[0, 0] = 99
print(x2_sub)
[[99  5]
 [ 7  6]]
In [ ]:
print(x2)
[[99  5  2  4]
 [ 7  6  8  8]
 [ 1  6  7  7]]

This default behavior is actually quite useful: it means that when we work with large datasets, we can access and process pieces of these datasets without the need to copy the underlying data buffer.

Creating copies of arrays

Despite the nice features of array views, it is sometimes useful to instead explicitly copy the data within an array or a subarray. This can be most easily done with the copy() method:

In [ ]:
x2_sub_copy = x2[:2, :2].copy()
print(x2_sub_copy)
[[99  5]
 [ 7  6]]

If we now modify this subarray, the original array is not touched:

In [ ]:
x2_sub_copy[0, 0] = 42
print(x2_sub_copy)
[[42  5]
 [ 7  6]]
In [ ]:
print(x2)
[[99  5  2  4]
 [ 7  6  8  8]
 [ 1  6  7  7]]

Reshaping of Arrays

Another useful type of operation is reshaping of arrays. The most flexible way of doing this is with the reshape method. For example, if you want to put the numbers 1 through 9 in a $3 \times 3$ grid, you can do the following:

In [ ]:
grid = np.arange(1, 10).reshape((3, 3))
print(grid)
[[1 2 3]
 [4 5 6]
 [7 8 9]]

Note that for this to work, the size of the initial array must match the size of the reshaped array. Where possible, the reshape method will use a no-copy view of the initial array, but with non-contiguous memory buffers this is not always the case.

Another common reshaping pattern is the conversion of a one-dimensional array into a two-dimensional row or column matrix. This can be done with the reshape method, or more easily done by making use of the newaxis keyword within a slice operation:

In [ ]:
x = np.array([1, 2, 3])

# row vector via reshape
x.reshape((1, 3))
Out[ ]:
array([[1, 2, 3]])
In [ ]:
# row vector via newaxis
x[np.newaxis, :]
Out[ ]:
array([[1, 2, 3]])
In [ ]:
# column vector via reshape
x.reshape((3, 1))
Out[ ]:
array([[1],
       [2],
       [3]])
In [ ]:
# column vector via newaxis
x[:, np.newaxis]
Out[ ]:
array([[1],
       [2],
       [3]])

We will see this type of transformation often throughout the remainder of the book.

Array Concatenation and Splitting

All of the preceding routines worked on single arrays. It's also possible to combine multiple arrays into one, and to conversely split a single array into multiple arrays. We'll take a look at those operations here.

Concatenation of arrays

Concatenation, or joining of two arrays in NumPy, is primarily accomplished using the routines np.concatenate, np.vstack, and np.hstack. np.concatenate takes a tuple or list of arrays as its first argument, as we can see here:

In [ ]:
x = np.array([1, 2, 3])
y = np.array([3, 2, 1])
np.concatenate([x, y])
Out[ ]:
array([1, 2, 3, 3, 2, 1])

You can also concatenate more than two arrays at once:

In [ ]:
z = [99, 99, 99]
print(np.concatenate([x, y, z]))
[ 1  2  3  3  2  1 99 99 99]

It can also be used for two-dimensional arrays:

In [ ]:
grid = np.array([[1, 2, 3],
                 [4, 5, 6]])
In [ ]:
# concatenate along the first axis
np.concatenate([grid, grid])
Out[ ]:
array([[1, 2, 3],
       [4, 5, 6],
       [1, 2, 3],
       [4, 5, 6]])
In [ ]:
# concatenate along the second axis (zero-indexed)
np.concatenate([grid, grid], axis=1)
Out[ ]:
array([[1, 2, 3, 1, 2, 3],
       [4, 5, 6, 4, 5, 6]])

For working with arrays of mixed dimensions, it can be clearer to use the np.vstack (vertical stack) and np.hstack (horizontal stack) functions:

In [ ]:
x = np.array([1, 2, 3])
grid = np.array([[9, 8, 7],
                 [6, 5, 4]])

# vertically stack the arrays
np.vstack([x, grid])
Out[ ]:
array([[1, 2, 3],
       [9, 8, 7],
       [6, 5, 4]])
In [ ]:
# horizontally stack the arrays
y = np.array([[99],
              [99]])
np.hstack([grid, y])
Out[ ]:
array([[ 9,  8,  7, 99],
       [ 6,  5,  4, 99]])

Similary, np.dstack will stack arrays along the third axis.

Splitting of arrays

The opposite of concatenation is splitting, which is implemented by the functions np.split, np.hsplit, and np.vsplit. For each of these, we can pass a list of indices giving the split points:

In [ ]:
x = [1, 2, 3, 99, 99, 3, 2, 1]
x1, x2, x3 = np.split(x, [3, 5])
print(x1, x2, x3)
[1 2 3] [99 99] [3 2 1]

Notice that N split-points, leads to N + 1 subarrays. The related functions np.hsplit and np.vsplit are similar:

In [ ]:
grid = np.arange(16).reshape((4, 4))
grid
Out[ ]:
array([[ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11],
       [12, 13, 14, 15]])
In [ ]:
upper, lower = np.vsplit(grid, [2])
print(upper)
print(lower)
[[0 1 2 3]
 [4 5 6 7]]
[[ 8  9 10 11]
 [12 13 14 15]]
In [ ]:
left, right = np.hsplit(grid, [2])
print(left)
print(right)
[[ 0  1]
 [ 4  5]
 [ 8  9]
 [12 13]]
[[ 2  3]
 [ 6  7]
 [10 11]
 [14 15]]

Similarly, np.dsplit will split arrays along the third axis.

Mathematical Operations with Arrays

As with vanilla python we have several possibilities to apply mathematical operations to our arrays. +, -, *, / and ** can be used to perform element wise addition, subtraction, multiplication, division and power.

To be able to do so, several conditions must hold:

  • Array shapes must be identical (generally speaking)
  • Arrays must encompass the same data type
In [ ]:
x = np.array([1,2,3])
y = np.array([4,5,6])

print(x + y) # elementwise addition     [1 2 3] + [4 5 6] = [5  7  9]
print(x - y) # elementwise subtraction  [1 2 3] - [4 5 6] = [-3 -3 -3]
[5 7 9]
[-3 -3 -3]
In [ ]:
print(x * y) # elementwise multiplication  [1 2 3] * [4 5 6] = [4  10  18]
print(x / y) # elementwise divison         [1 2 3] / [4 5 6] = [0.25  0.4  0.5]
[ 4 10 18]
[0.25 0.4  0.5 ]
In [ ]:
print(x**2) # elementwise power  [1 2 3] ^2 =  [1 4 9]
[1 4 9]
In [ ]:
z = np.array(["1", "2", "3"])

print(x + z) # this will fail since array have different data type
<class 'numpy.ndarray'>
---------------------------------------------------------------------------
UFuncTypeError                            Traceback (most recent call last)
<ipython-input-109-757dc692d51a> in <module>()
      2 print(type(x))
      3 
----> 4 print(x + z) # this will fail since array have different data type

UFuncTypeError: ufunc 'add' did not contain a loop with signature matching types (dtype('<U21'), dtype('<U21')) -> dtype('<U21')

Dot Product:

As you may have guessed, arrays are often used to represent vectors in linear algebra. We can therefore also do a dot products between a scalar and a vector.

$ \begin{bmatrix}x_1 \ x_2 \ x_3\end{bmatrix} \cdot \begin{bmatrix}y_1 \\ y_2 \\ y_3\end{bmatrix} = x_1 y_1 + x_2 y_2 + x_3 y_3$

In [ ]:
print(x)
print(y)
x.dot(y) # dot product  1*4 + 2*5 + 3*6
[1 2 3]
[4 5 6]
Out[ ]:
32