3  NumPy and Xarray

3.1 Topics Covered

  1. NumPy data types
  2. Data type, size, shape, and number of dimensions of an array
  3. Creating, reshaping, and slicing NumPy arrays
  4. Numeric and comparison operations on arrays
  5. How Xarray differs from NumPy
  6. Creating, subsetting, and summarizing labeled arrays with Xarray

3.2 Introduction

The NumPy and Pandas libraries are central to data science in Python. NumPy allows for the efficient analysis and processing of data arrays with varying sizes, shapes, and numbers of dimensions while Pandas allows for reading in and working with data tables. In this chapter, we focus on NumPy. We also introduce Xarray, which builds on NumPy by adding labeled dimensions and coordinates to arrays. Pandas is discussed in Chapter 4.

3.3 Creating NumPy Arrays

The NumPy library allows for creating and working with arrays. It is of specific value when you want to perform mathematical operations on values stored in arrays. It is also very fast and memory efficient due to its reliance on the C language. As mentioned in Chapter 1, arrays are similar to lists in that they store a series of values or elements. However, arrays can be expanded to include many dimensions. For example, an image could be represented as an array with three dimensions: height, width, and channels. If you work with deep learning, tensors are the primary data model used to read and manipulate data and are essentially multidimensional arrays that can be stored in RAM or within GPU memory for faster computation. In short, array-based calculations and manipulations are essential to data science, so NumPy is an important library to learn if you work in the Python environment.

The complete documentation for NumPy can be found here.

Before you can use NumPy, you must make sure that it is installed into your conda environment, as described in the Preface. Once NumPy is installed, you will need to import it in order to use it in your code. It is common to assign NumPy an alias of np to simplify your code.

import numpy as np

Lists can be converted to NumPy arrays using the array() function. Once the list object is converted to an array, the type is defined as numpy.ndarray, which indicates that it is a NumPy array specifically. Since this array only has one dimension, it is specifically called a vector.

lst1 = [3, 6, 7, 8, 9]
arr1 = np.array(lst1)
print(type(lst1))
<class 'list'>
print(type(arr1))
<class 'numpy.ndarray'>
print(arr1)
[3 6 7 8 9]

A two-dimensional array is known as a matrix. In the example below, we generate a matrix from a list of lists.

lst2 = [[3, 6, 7, 8, 9], [3, 6, 7, 8, 9], [3, 6, 7, 8, 9]]
arr2 = np.array(lst2)
print(arr2)
[[3 6 7 8 9]
 [3 6 7 8 9]
 [3 6 7 8 9]]

Again, one of the powerful advantages of NumPy arrays is the ability to store data in arrays with many dimensions. In the example below, we create a three-dimensional array from a list of lists of lists. This would be similar to an image with dimensions of image height, image width, and image channels (for example, red, green, and blue). A four-dimensional array could represent a time series (height, width, channels, and time) or a video containing multiple frames (frame height, frame width, channels, and frame number).

lst3 = [[[3, 6, 7, 8, 9], [3, 6, 7, 8, 9], [3, 6, 7, 8, 9]],
         [[3, 6, 7, 8, 9], [3, 6, 7, 8, 9], [3, 6, 7, 8, 9]],
         [[3, 6, 7, 8, 9], [3, 6, 7, 8, 9], [3, 6, 7, 8, 9]]]
arr3 = np.array(lst3)
print(arr3)
[[[3 6 7 8 9]
  [3 6 7 8 9]
  [3 6 7 8 9]]

 [[3 6 7 8 9]
  [3 6 7 8 9]
  [3 6 7 8 9]]

 [[3 6 7 8 9]
  [3 6 7 8 9]
  [3 6 7 8 9]]]

The cell below provides some examples of NumPy functions for generating arrays.

The arange() function returns an array of evenly spaced values and accepts start, stop, step, and data type parameters. In the example, we create an array of evenly spaced values from 0 to 100 with a step size of 5. We use 101 as opposed to 100 since the last value in the provided range is not included. We specifically define the data type as integer, but NumPy can infer a data type if it is not provided.

The linspace() function is similar to arange(); however, a number of samples is specified as opposed to a step size. In the example, since 5 samples are requested, 5 evenly spaced values between 0 and 100 are returned.

The ones() function is used to return an array of 1s. In the example, we generate a three-dimensional array where the first dimension has a length of 3, the second a length of 4, and the third a length of 4. The shape and dimensions of the array are specified using a tuple.

Similar to ones(), zeros() generates an array of zeros.

It is also possible to generate random values between 0 and 1 (random.rand()) and a specified number of random integer values between two values (random.randint()).

arr4 = np.arange(0, 101, 5, dtype="int")
print(arr4)
[  0   5  10  15  20  25  30  35  40  45  50  55  60  65  70  75  80  85
  90  95 100]
arr5 = np.linspace(0, 100, 5, dtype="int")
print(arr5)
[  0  25  50  75 100]
arr6 = np.ones((3, 4, 4))
print(arr6)
[[[1. 1. 1. 1.]
  [1. 1. 1. 1.]
  [1. 1. 1. 1.]
  [1. 1. 1. 1.]]

 [[1. 1. 1. 1.]
  [1. 1. 1. 1.]
  [1. 1. 1. 1.]
  [1. 1. 1. 1.]]

 [[1. 1. 1. 1.]
  [1. 1. 1. 1.]
  [1. 1. 1. 1.]
  [1. 1. 1. 1.]]]
arr7 = np.zeros((3, 4, 2))
print(arr7)
[[[0. 0.]
  [0. 0.]
  [0. 0.]
  [0. 0.]]

 [[0. 0.]
  [0. 0.]
  [0. 0.]
  [0. 0.]]

 [[0. 0.]
  [0. 0.]
  [0. 0.]
  [0. 0.]]]
arr8 = np.random.rand(3, 4, 5)
print(arr8)
[[[0.88609588 0.83451541 0.51107762 0.36350961 0.7258199 ]
  [0.40608208 0.24466723 0.16234896 0.23221965 0.09050055]
  [0.13016631 0.58720394 0.15044583 0.21939141 0.78378492]
  [0.14127942 0.46236845 0.84406729 0.84216381 0.63389861]]

 [[0.25337333 0.76563718 0.65080751 0.23369803 0.09891386]
  [0.20027669 0.81924743 0.74921102 0.10899274 0.90617973]
  [0.39937862 0.1401036  0.58231025 0.74309508 0.60386222]
  [0.83866278 0.20860347 0.83738251 0.75463004 0.43994647]]

 [[0.83773455 0.13222103 0.37260129 0.20028061 0.60999762]
  [0.18556061 0.12699235 0.88211062 0.8376589  0.42964443]
  [0.33175242 0.3369349  0.18649587 0.03069557 0.53719916]
  [0.08406271 0.95630543 0.51588475 0.10103673 0.72980306]]]
arr9 = np.random.randint(1, 200, 7)
print(arr9)
[ 30 126 183 173  40 113  10]

3.4 NumPy Data Types

NumPy provides additional and more specific data types in comparison to base Python. Here, we provide a brief explanation of commonly used data types.

  • bool_: Boolean True or False
  • int8: 8-bit signed integer (-128 to 127)
  • int16: 16-bit signed integer (-32,768 to 32,767)
  • int32: 32-bit signed integer (-2,147,483,648 to 2,147,483,647)
  • int64: 64-bit signed integer (-9,223,372,036,854,775,808 to 9,223,372,036,854,775,807)
  • uint8: 8-bit unsigned integer (0 to 255)
  • uint16: 16-bit unsigned integer (0 to 65,535)
  • uint32: 32-bit unsigned integer (0 to 4,294,967,295)
  • uint64: 64-bit unsigned integer (0 to 18,446,744,073,709,551,615)
  • float16: half precision float
  • float32: single precision float
  • float64: double precision float

Signed integers can differentiate positive and negative values while unsigned integers cannot. Float data can store decimal values while integer data cannot. There are also data types for complex numbers, which we do not discuss here.

Below, we demonstrate how to define the data type with the dtype parameter. In all cases, we use ones() to create an array with three elements. For both int8 and int16, 1 as an integer value is returned. When the data are defined as float16, 1 as a float value is returned (1.). Lastly, when the type is set to bool_, Boolean True is returned since 1 indicates True and 0 indicates False.

The data type impacts the amount of memory needed. For example, an int8 requires less memory than an int16. We generally try to use the data type that can represent the data with the least amount of memory unless a specific data type is needed in an analysis.

arr1 = np.ones((3), dtype="int8")
print(arr1)
[1 1 1]
print(arr1.dtype)
int8
arr1 = np.ones((3), dtype="int16")
print(arr1)
[1 1 1]
print(arr1.dtype)
int16
arr1 = np.ones((3), dtype="float16")
print(arr1)
[1. 1. 1.]
print(arr1.dtype)
float16
arr1 = np.ones((3), dtype="bool_")
print(arr1)
[ True  True  True]
print(arr1.dtype)
bool

3.5 Understanding and Manipulating Array Shape and Dimensions

Let’s spend some time discussing the dimensions and shape of an array. The shape of an array relates to the length of each dimension. The len() function returns the length of the first dimension (in this case 3). To obtain a tuple of the lengths for all dimensions, you must use the shape property. So, the array generated has three dimensions with lengths of 3, 4, and 4, respectively. The number of dimensions is returned with the ndim property. The size property returns the number of elements in the array. There are 48 elements in the example array: 3 X 4 X 4 = 48. The dtype property provides the data type.

arr6 = np.ones((3, 4, 4))

print("Length of first dimension: " + str(len(arr6)))
Length of first dimension: 3
print("Shape of array: " + str(arr6.shape))
Shape of array: (3, 4, 4)
print("Number of dimensions: " + str(arr6.ndim))
Number of dimensions: 3
print("Size of array: " + str(arr6.size))
Size of array: 48
print("Data type of array: " + str(arr6.dtype))
Data type of array: float64

NumPy has a built-in method for changing the shape of an array: reshape(). Note that the number of elements, or size, of the array must perfectly fill the new shape. In the first example, we maintain the number of dimensions but change the shape or length of each dimension. In the next two examples, we convert the three-dimensional array to two-dimensional arrays. Lastly, we convert the array to a one-dimensional array, or vector, with a length of 48.

arr6b = arr6.reshape(4, 4, 3)
arr6c = arr6.reshape(4, 12)
arr6d = arr6.reshape(12, 4)
arr6e = arr6.reshape(48)
print(arr6b)
[[[1. 1. 1.]
  [1. 1. 1.]
  [1. 1. 1.]
  [1. 1. 1.]]

 [[1. 1. 1.]
  [1. 1. 1.]
  [1. 1. 1.]
  [1. 1. 1.]]

 [[1. 1. 1.]
  [1. 1. 1.]
  [1. 1. 1.]
  [1. 1. 1.]]

 [[1. 1. 1.]
  [1. 1. 1.]
  [1. 1. 1.]
  [1. 1. 1.]]]
print(arr6c)
[[1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]
 [1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]
 [1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]
 [1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]]
print(arr6d)
[[1. 1. 1. 1.]
 [1. 1. 1. 1.]
 [1. 1. 1. 1.]
 [1. 1. 1. 1.]
 [1. 1. 1. 1.]
 [1. 1. 1. 1.]
 [1. 1. 1. 1.]
 [1. 1. 1. 1.]
 [1. 1. 1. 1.]
 [1. 1. 1. 1.]
 [1. 1. 1. 1.]
 [1. 1. 1. 1.]]
print(arr6e)
[1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1.
 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]

As a second example, here we convert a vector into a matrix.

arr10 = np.random.randint(1, 1200, 100)
arr10b = arr10.reshape(10, 10)
print(arr10b)
[[ 481  685  889  158  275  638  408   60  742 1055]
 [  94   83  458  451  185  459  989  903  527  782]
 [ 707  624  313  256  575    7  108  301  421  829]
 [ 331  525  689  695  822  129   71  696  912  397]
 [ 594  393  313  246  842  905  606  214  567  222]
 [  79   28  258  287  885  568  385  771  485  867]
 [ 920   63   96 1016  611   54 1162  415   23   32]
 [1006  226 1183 1163  258  664  509  913  319  570]
 [ 206 1134 1101  734 1104  161 1181  243 1149  772]
 [ 126 1071  428  598  820 1007  474 1119  593  203]]

When reshaping, it is possible to have NumPy determine the appropriate size of a single dimension to fill an array with the available elements. This is accomplished using -1 in the array dimension location when applying the reshape() method.

arr10 = np.random.randint(1, 1200, 1000)
arr10b = arr10.reshape(-1, 10, 10)
arr10c = arr10.reshape(10, -1, 10)
arr10d = arr10.reshape(10, 10, -1)
print(arr10.shape)
(1000,)
print(arr10b.shape)
(10, 10, 10)
print(arr10c.shape)
(10, 10, 10)
print(arr10d.shape)
(10, 10, 10)

3.6 NumPy Array Indexing

Similar to lists, NumPy arrays are indexed. So, values from the array can be extracted or referenced using their associated index. Since arrays often have multiple dimensions, indexes must also extend into multiple dimensions. See the comments below for general array indexing rules. Remember that indexing starts at 0, the first index provided is included, and the last index provided is not included. Extracting portions of an array is known as slicing.

arr11 = np.linspace(0, 50, 50, dtype="int")
arr12 = arr11.reshape(2,5,5)
print("Original array")
Original array
print(arr12)
[[[ 0  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 26 27 28 29]
  [30 31 32 33 34]
  [35 36 37 38 39]
  [40 41 42 43 44]
  [45 46 47 48 50]]]
print("All values in first index of first dimension")
All values in first index of first dimension
print(arr12[0]) #This will extract just the values from the first index in the first 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 24]]
print("All values in second index of first dimension")
All values in second index of first dimension
print(arr12[1]) #This will extract just the values from the second index in the first dimension
[[25 26 27 28 29]
 [30 31 32 33 34]
 [35 36 37 38 39]
 [40 41 42 43 44]
 [45 46 47 48 50]]
print("All values in first index of first dimension and first index of second dimension")
All values in first index of first dimension and first index of second dimension
print(arr12[0][0]) #This will extract all values occurring in the first index of both the first and second dimensions
[0 1 2 3 4]
print("A single value specified with three indexes, one for each dimension")
A single value specified with three indexes, one for each dimension
print(arr12[1, 3, 3]) #This will extract a specific value based on an index in all three dimensions
43
print("Incorporating ranges")
Incorporating ranges
print(arr12[1, 0:2, 0:2]) #All values in second index of first dimension that are also included in the first to second index of the second and third dimensions
[[25 26]
 [30 31]]
print("Using colons")
Using colons
print(arr12[:, 0:2, 0:2]) #Only a colon means select all values in a dimension
[[[ 0  1]
  [ 5  6]]

 [[25 26]
  [30 31]]]
print(arr12[:,2:,0:2]) #Can also use colons to select all values before or after an index
[[[10 11]
  [15 16]
  [20 21]]

 [[35 36]
  [40 41]
  [45 46]]]

Once values have been selected using index notation, they can be changed. In the example below, we convert all values in the first index of the first dimension and the first index of the second dimension to 0.

arr12[0][0] = 0
print(arr12)
[[[ 0  0  0  0  0]
  [ 5  6  7  8  9]
  [10 11 12 13 14]
  [15 16 17 18 19]
  [20 21 22 23 24]]

 [[25 26 27 28 29]
  [30 31 32 33 34]
  [35 36 37 38 39]
  [40 41 42 43 44]
  [45 46 47 48 50]]]

3.7 Boolean Arrays

It is also possible to create arrays of Boolean values, as demonstrated below.

arr13 = np.array([True, False, True, False, True, False, True, False, False])
arr13b = arr13.reshape(3, 3)
print(arr13b)
[[ True False  True]
 [False  True False]
 [ True False False]]

Comparison operators can be used to compare each value in an array to a value and return the Boolean result to the associated position in a new array.

arr10 = np.random.randint(1, 1200, 100)
arr10b = arr10.reshape(10, 10)
print(arr10b)
[[ 372 1135  607  180  515  197  188  811  962  546]
 [ 543  809  933  871 1136  317  488  939  553 1070]
 [1179  371  309  829  932  319 1004  930 1062  769]
 [ 145  497  616  440  733  622 1039   96  344  728]
 [ 222 1092  346  912  140  923  904  807 1102  497]
 [ 280  333 1190  326  153   14 1163   65  506  957]
 [1080  915 1185  313  666  458  212 1049   32  910]
 [   7  801  595 1003  359 1164  556   83   62 1083]
 [ 174  416  923  746  144  933  716 1156  919 1124]
 [1159  651  369 1047  754  263   47  650  750   96]]
arr10bool = arr10b > 150
print(arr10bool)
[[ True  True  True  True  True  True  True  True  True  True]
 [ True  True  True  True  True  True  True  True  True  True]
 [ True  True  True  True  True  True  True  True  True  True]
 [False  True  True  True  True  True  True False  True  True]
 [ True  True  True  True False  True  True  True  True  True]
 [ True  True  True  True  True False  True False  True  True]
 [ True  True  True  True  True  True  True  True False  True]
 [False  True  True  True  True  True  True False False  True]
 [ True  True  True  True False  True  True  True  True  True]
 [ True  True  True  True  True  True False  True  True False]]

3.8 Copy vs. View

In Chapter 1, we explained how, for mutable data types, setting a variable equal to another variable results in a reference to the original object or data in memory. So, changes to the original object or the new object will change both since they reference the same data.

The behavior for NumPy arrays is similar. Two methods are available for creating a new variable relative to an existing variable: copy() and view().

When using view(), the variable will reference the same data or object in memory. So, changes to the original variable or the new variable created using view() will result in changes to both. Using view(), you can also reference portions of an array. This allows you to work with a subset of the data values without copying or replicating the data in memory.

In contrast to view(), copy() will copy the data or object in memory, so changes made to either the original or the copied object will not impact the other.

The three examples below demonstrate this behavior. In the first example, arr2 is created as a view of arr1 while arr3 is created as a copy of arr1. A subsequent change to arr1 changes arr1 and arr2 but not arr3. In the second example, a change to arr2, a view of arr1, impacts both arr1 and arr2 but not arr3. Lastly, changes to arr3 impact only arr3 and not arr1 or arr2 since it is a copy of arr1 as opposed to a view.

In summary, if you want to make a copy of an array as opposed to referencing the original data, you should use the copy() method.

import copy

arr1 = np.array(25)
arr2 = np.array(25)
arr3 = np.array(25)

arr1 = np.random.randint(1, 100, 25)
arr1 = arr1.reshape(5, 5)
print(arr1)
[[79 32 32 34 49]
 [70 79 93 37  8]
 [19 18 71 75 87]
 [34 61 61 75 91]
 [85 19 23 10 18]]
arr2 = arr1.view()
arr3 = arr1.copy()
arr1[:, :] = 0
print(arr1)
[[0 0 0 0 0]
 [0 0 0 0 0]
 [0 0 0 0 0]
 [0 0 0 0 0]
 [0 0 0 0 0]]
print(arr2)
[[0 0 0 0 0]
 [0 0 0 0 0]
 [0 0 0 0 0]
 [0 0 0 0 0]
 [0 0 0 0 0]]
print(arr3)
[[79 32 32 34 49]
 [70 79 93 37  8]
 [19 18 71 75 87]
 [34 61 61 75 91]
 [85 19 23 10 18]]
import copy

arr1 = np.array(25)
arr2 = np.array(25)
arr3 = np.array(25)

arr1 = np.random.randint(1, 100, 25)
arr1 = arr1.reshape(5, 5)
print(arr1)
[[86  2 78 30 99]
 [93 32 61 91 23]
 [84 81 60  9 89]
 [10 95 11 21 84]
 [26 19 23  4 80]]
arr2 = arr1.view()
arr3 = arr1.copy()
arr2[:, :] = 0
print(arr1)
[[0 0 0 0 0]
 [0 0 0 0 0]
 [0 0 0 0 0]
 [0 0 0 0 0]
 [0 0 0 0 0]]
print(arr2)
[[0 0 0 0 0]
 [0 0 0 0 0]
 [0 0 0 0 0]
 [0 0 0 0 0]
 [0 0 0 0 0]]
print(arr3)
[[86  2 78 30 99]
 [93 32 61 91 23]
 [84 81 60  9 89]
 [10 95 11 21 84]
 [26 19 23  4 80]]
import copy

arr1 = np.array(25)
arr2 = np.array(25)
arr3 = np.array(25)

arr1 = np.random.randint(1, 100, 25)
arr1 = arr1.reshape(5, 5)
print(arr1)
[[51 15 46 46 67]
 [17 11 14  2 52]
 [86 55 50 74  5]
 [48 34 17 52 41]
 [83 26 16 11 32]]
arr2 = arr1.view()
arr3 = arr1.copy()
arr3[:, :] = 0
print(arr1)
[[51 15 46 46 67]
 [17 11 14  2 52]
 [86 55 50 74  5]
 [48 34 17 52 41]
 [83 26 16 11 32]]
print(arr2)
[[51 15 46 46 67]
 [17 11 14  2 52]
 [86 55 50 74  5]
 [48 34 17 52 41]
 [83 26 16 11 32]]
print(arr3)
[[0 0 0 0 0]
 [0 0 0 0 0]
 [0 0 0 0 0]
 [0 0 0 0 0]
 [0 0 0 0 0]]

3.9 Array Arithmetic and Operations

It is generally easy to perform mathematical operations on arrays, as demonstrated below. In all cases, the same operation is applied to all elements in the array.

arr14 = np.random.randint(1, 1200, 25)
arr14b = arr14.reshape(5, 5)
print(arr14b)
[[ 593  526  654 1197 1037]
 [ 353 1010 1160  823  871]
 [ 301  720 1163  640   40]
 [1099  407  450  862  260]
 [1195  403  429  829  926]]
print(arr14b+21)
[[ 614  547  675 1218 1058]
 [ 374 1031 1181  844  892]
 [ 322  741 1184  661   61]
 [1120  428  471  883  281]
 [1216  424  450  850  947]]
print(arr14b-52)
[[ 541  474  602 1145  985]
 [ 301  958 1108  771  819]
 [ 249  668 1111  588  -12]
 [1047  355  398  810  208]
 [1143  351  377  777  874]]
print(arr14b*2)
[[1186 1052 1308 2394 2074]
 [ 706 2020 2320 1646 1742]
 [ 602 1440 2326 1280   80]
 [2198  814  900 1724  520]
 [2390  806  858 1658 1852]]
print(arr14b/3)
[[197.66666667 175.33333333 218.         399.         345.66666667]
 [117.66666667 336.66666667 386.66666667 274.33333333 290.33333333]
 [100.33333333 240.         387.66666667 213.33333333  13.33333333]
 [366.33333333 135.66666667 150.         287.33333333  86.66666667]
 [398.33333333 134.33333333 143.         276.33333333 308.66666667]]
print(arr14b**2)
[[ 351649  276676  427716 1432809 1075369]
 [ 124609 1020100 1345600  677329  758641]
 [  90601  518400 1352569  409600    1600]
 [1207801  165649  202500  743044   67600]
 [1428025  162409  184041  687241  857476]]

It is also possible to perform mathematical operations on sets of arrays as long as they have the same shape. In such cases, elements are matched based on having the same position within the array.

arr14 = np.random.randint(1, 1200, 25)
arr14b = arr14.reshape(5, 5)
print(arr14b)
[[ 398 1019  732  162 1115]
 [ 261  838  200  916  224]
 [ 747  583  794  442  335]
 [  61  781  127  482  195]
 [  47  288  499  116 1162]]
print(arr14b+arr14b)
[[ 796 2038 1464  324 2230]
 [ 522 1676  400 1832  448]
 [1494 1166 1588  884  670]
 [ 122 1562  254  964  390]
 [  94  576  998  232 2324]]
print(arr14b-arr14b)
[[0 0 0 0 0]
 [0 0 0 0 0]
 [0 0 0 0 0]
 [0 0 0 0 0]
 [0 0 0 0 0]]

To summarize the results from above, it is possible to:

  1. perform mathematical operations between an array with any shape and a scalar (i.e., a single value).
  2. perform mathematical operations between arrays that have the same shape.

There are some other cases in which it is possible to perform mathematical operations using a technique known as broadcasting. The following rules summarize when broadcasting can be used and how.

  1. If two arrays have a different number of dimensions, the shape of the array with fewer dimensions is padded with ones on its leading side (for example, to multiply an array of shape (3) by an array of shape (3,3), the first array is treated as having shape (1,3)).
  2. If the shape of the arrays does not match in a dimension, the array with shape equal to 1 in that dimension is stretched to match the other shape.
  3. If in any dimension the sizes disagree and neither is equal to 1, an error is raised.

In the example below, an array of shape (6, 6) is multiplied by an array of shape (6). This requires that the second array be broadcast to a shape of (1, 6).

arr1 = np.random.randint(1, 100, 36)
arr1b = arr1.reshape(6, 6)

arr2 = np.ones((6))
arr2[:] = 2

print(arr1b)
[[75 31  9 93 27 63]
 [86 23 34 41 44 33]
 [97 58 60 86 74 49]
 [87 39 18  8 59 49]
 [56 63 85 95 45 35]
 [62 79 70 84 80 24]]
print(arr2)
[2. 2. 2. 2. 2. 2.]
print(arr1b*arr2)
[[150.  62.  18. 186.  54. 126.]
 [172.  46.  68.  82.  88.  66.]
 [194. 116. 120. 172. 148.  98.]
 [174.  78.  36.  16. 118.  98.]
 [112. 126. 170. 190.  90.  70.]
 [124. 158. 140. 168. 160.  48.]]

NumPy provides mathematical functions and methods for performing common tasks. The block of code below provides some examples.

arr14 = np.random.randint(1, 1200, 25)
arr14b = arr14.reshape(5, 5)

print(np.max(arr14b))
1148
print(np.min(arr14b))
14
print(np.sqrt(arr14b))
[[20.4450483  22.97825059 23.04343724 24.85960579 23.60084744]
 [10.86278049 30.33150178 30.46309242 28.82707061  3.74165739]
 [16.0623784  18.35755975 33.88214869 25.84569597 32.24903099]
 [ 8.48528137  9.05538514 33.86738844 17.29161647 14.03566885]
 [15.45962483 30.46309242 15.42724862 25.11971337 13.7113092 ]]

3.10 Xarray

3.10.1 How Xarray Differs from NumPy

NumPy arrays are fast and flexible, but they only keep track of the position of each value. A NumPy array does not know what its dimensions represent. When working with geospatial and environmental data, however, dimensions almost always have meaning. A multispectral image has band, row (y), and column (x) dimensions, and a stack of monthly climate grids has time, latitude, and longitude dimensions. With NumPy, it is up to you to remember that axis 0 is time, axis 1 is y, and axis 2 is x. It is also up to you to separately store the dates, coordinates, and units associated with the data. This makes it easy to make mistakes, such as calculating a mean over the wrong axis.

The Xarray library addresses these issues by adding labels to NumPy arrays. Here are the key differences between Xarray and NumPy:

  • Named dimensions: Each dimension is given a name, such as “time”, “y”, or “x”. Operations can then reference dimensions by name (for example, mean(dim="time")) as opposed to by axis number (mean(axis=0)).
  • Coordinates: Each position along a dimension can be assigned a label, such as a date, a map coordinate, or a band wavelength. Data can then be selected using these labels as opposed to only using indexes.
  • Attributes: Arbitrary metadata, such as units or a description, can be stored with the data.
  • Broadcasting and alignment by name: When performing operations on multiple arrays, Xarray matches dimensions based on their names as opposed to their order and shape.
  • Datasets: Multiple related variables that share dimensions and coordinates can be stored together in a single object, similar to how a NetCDF file stores multiple variables.
  • File input/output: Xarray can read and write common scientific data formats, such as NetCDF and Zarr, and, with the rioxarray extension, geospatial raster formats such as GeoTIFF. It can also work with datasets that are too large to fit in memory using Dask.

Xarray does not replace NumPy. The data in an Xarray object are stored as a NumPy array, and most NumPy functions can be applied to Xarray objects directly. In general, we use NumPy for lower-level array manipulation and when preparing data for other libraries that expect NumPy arrays, such as scikit-learn. We use Xarray when working with labeled, multidimensional data, such as time series of raster grids or climate model output.

Xarray provides two primary data structures:

  • DataArray: a single labeled, multidimensional array (the labeled equivalent of a NumPy array)
  • Dataset: a collection of DataArrays that share dimensions and coordinates

It is common to assign Xarray an alias of xr.

import xarray as xr

3.10.2 Creating a DataArray

To compare NumPy and Xarray, we recreate the three-dimensional array that was used above to demonstrate indexing. Imagine that this array represents two monthly grids of mean temperature in degrees Celsius, one for June and one for July, each with 5 rows and 5 columns of cells.

A DataArray is created using the DataArray() function. In the example, we provide the NumPy array, a tuple of dimension names, a dictionary of coordinates for each dimension, a name for the variable, and a dictionary of attributes. The y coordinates decrease from the top to the bottom of the grid, which is the convention for raster data. Note that the printed DataArray includes the dimension names and lengths, the data values, the coordinates, and the attributes.

arr12 = np.linspace(0, 50, 50, dtype="int").reshape(2, 5, 5)

da12 = xr.DataArray(
    arr12,
    dims=("time", "y", "x"),
    coords={
        "time": np.array(["2024-06-01", "2024-07-01"], dtype="datetime64[ns]"),
        "y": [500, 400, 300, 200, 100],
        "x": [10, 20, 30, 40, 50]
    },
    name="temp",
    attrs={"units": "degrees C", "description": "Monthly mean temperature"}
)
print(da12)
<xarray.DataArray 'temp' (time: 2, y: 5, x: 5)> Size: 400B
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, 24]],

       [[25, 26, 27, 28, 29],
        [30, 31, 32, 33, 34],
        [35, 36, 37, 38, 39],
        [40, 41, 42, 43, 44],
        [45, 46, 47, 48, 50]]])
Coordinates:
  * time     (time) datetime64[ns] 16B 2024-06-01 2024-07-01
  * y        (y) int64 40B 500 400 300 200 100
  * x        (x) int64 40B 10 20 30 40 50
Attributes:
    units:        degrees C
    description:  Monthly mean temperature

Many of the properties available for NumPy arrays are also available for DataArrays. In addition, the dims property returns the dimension names, and the sizes property returns the length of each dimension associated with its name. The values property returns the underlying data as a NumPy array.

print("Dimension names: " + str(da12.dims))
Dimension names: ('time', 'y', 'x')
print("Shape of array: " + str(da12.shape))
Shape of array: (2, 5, 5)
print("Dimension sizes: " + str(dict(da12.sizes)))
Dimension sizes: {'time': 2, 'y': 5, 'x': 5}
print("Number of dimensions: " + str(da12.ndim))
Number of dimensions: 3
print("Size of array: " + str(da12.size))
Size of array: 50
print("Data type of array: " + str(da12.dtype))
Data type of array: int64
print(type(da12.values))
<class 'numpy.ndarray'>

Coordinates and attributes can be accessed using the coords and attrs properties or by using the coordinate name.

print(da12.coords)
Coordinates:
  * time     (time) datetime64[ns] 16B 2024-06-01 2024-07-01
  * y        (y) int64 40B 500 400 300 200 100
  * x        (x) int64 40B 10 20 30 40 50
print(da12.attrs)
{'units': 'degrees C', 'description': 'Monthly mean temperature'}
print(da12["time"].values)
['2024-06-01T00:00:00.000000000' '2024-07-01T00:00:00.000000000']

3.10.3 Indexing and Selecting

Xarray provides two methods for selecting data. The isel() method selects using integer indexes, similar to NumPy, while the sel() method selects using coordinate labels. In both cases, the dimension is referenced by name, so the order in which the dimensions are listed does not matter.

In the example below, the same value is extracted in three ways: (1) using NumPy indexing, (2) using isel() and indexes, and (3) using sel() and coordinate labels. Note that the last two dimensions are provided in reverse order in the sel() example, which is not a problem since the dimensions are referenced by name. The item() method returns the single value as a standard Python number.

print(arr12[1, 3, 3])
43
print(da12.isel(time=1, y=3, x=3).item())
43
print(da12.sel(time="2024-07-01", x=40, y=200).item())
43

Ranges are defined using slice(). When using isel(), slices behave like NumPy: the first index is included and the last index is not. In contrast, when using sel(), both the start and end labels are included. In the example, the first selection is equivalent to arr12[:, 0:2, 0:2], and the second selection returns all cells with x coordinates between 10 and 20, inclusive. Note that the coordinates are subset along with the data.

print(da12.isel(y=slice(0, 2), x=slice(0, 2)))
<xarray.DataArray 'temp' (time: 2, y: 2, x: 2)> Size: 64B
array([[[ 0,  1],
        [ 5,  6]],

       [[25, 26],
        [30, 31]]])
Coordinates:
  * time     (time) datetime64[ns] 16B 2024-06-01 2024-07-01
  * y        (y) int64 16B 500 400
  * x        (x) int64 16B 10 20
Attributes:
    units:        degrees C
    description:  Monthly mean temperature
print(da12.sel(x=slice(10, 20)))
<xarray.DataArray 'temp' (time: 2, y: 5, x: 2)> Size: 160B
array([[[ 0,  1],
        [ 5,  6],
        [10, 11],
        [15, 16],
        [20, 21]],

       [[25, 26],
        [30, 31],
        [35, 36],
        [40, 41],
        [45, 46]]])
Coordinates:
  * time     (time) datetime64[ns] 16B 2024-06-01 2024-07-01
  * y        (y) int64 40B 500 400 300 200 100
  * x        (x) int64 16B 10 20
Attributes:
    units:        degrees C
    description:  Monthly mean temperature

Since time is stored as a date, it is also possible to select using partial dates. For example, "2024-07" will select all data from July 2024.

print(da12.sel(time="2024-07"))
<xarray.DataArray 'temp' (time: 1, y: 5, x: 5)> Size: 200B
array([[[25, 26, 27, 28, 29],
        [30, 31, 32, 33, 34],
        [35, 36, 37, 38, 39],
        [40, 41, 42, 43, 44],
        [45, 46, 47, 48, 50]]])
Coordinates:
  * time     (time) datetime64[ns] 8B 2024-07-01
  * y        (y) int64 40B 500 400 300 200 100
  * x        (x) int64 40B 10 20 30 40 50
Attributes:
    units:        degrees C
    description:  Monthly mean temperature

3.10.4 Arithmetic and Summary Statistics

Mathematical operations work the same as with NumPy arrays. However, the result is a DataArray that maintains the dimension names and coordinates. NumPy functions, such as np.sqrt(), can also be applied directly to DataArrays. In the example, we convert the temperature values from degrees Celsius to degrees Fahrenheit. Since the units have changed, we update the attributes using assign_attrs().

da12_f = (da12 * 9/5 + 32).assign_attrs(units="degrees F")
print(da12_f)
<xarray.DataArray 'temp' (time: 2, y: 5, x: 5)> Size: 400B
array([[[ 32. ,  33.8,  35.6,  37.4,  39.2],
        [ 41. ,  42.8,  44.6,  46.4,  48.2],
        [ 50. ,  51.8,  53.6,  55.4,  57.2],
        [ 59. ,  60.8,  62.6,  64.4,  66.2],
        [ 68. ,  69.8,  71.6,  73.4,  75.2]],

       [[ 77. ,  78.8,  80.6,  82.4,  84.2],
        [ 86. ,  87.8,  89.6,  91.4,  93.2],
        [ 95. ,  96.8,  98.6, 100.4, 102.2],
        [104. , 105.8, 107.6, 109.4, 111.2],
        [113. , 114.8, 116.6, 118.4, 122. ]]])
Coordinates:
  * time     (time) datetime64[ns] 16B 2024-06-01 2024-07-01
  * y        (y) int64 40B 500 400 300 200 100
  * x        (x) int64 40B 10 20 30 40 50
Attributes:
    units:        degrees F
    description:  Monthly mean temperature
print(np.sqrt(da12))
<xarray.DataArray 'temp' (time: 2, y: 5, x: 5)> Size: 400B
array([[[0.        , 1.        , 1.41421356, 1.73205081, 2.        ],
        [2.23606798, 2.44948974, 2.64575131, 2.82842712, 3.        ],
        [3.16227766, 3.31662479, 3.46410162, 3.60555128, 3.74165739],
        [3.87298335, 4.        , 4.12310563, 4.24264069, 4.35889894],
        [4.47213595, 4.58257569, 4.69041576, 4.79583152, 4.89897949]],

       [[5.        , 5.09901951, 5.19615242, 5.29150262, 5.38516481],
        [5.47722558, 5.56776436, 5.65685425, 5.74456265, 5.83095189],
        [5.91607978, 6.        , 6.08276253, 6.164414  , 6.244998  ],
        [6.32455532, 6.40312424, 6.4807407 , 6.55743852, 6.63324958],
        [6.70820393, 6.78232998, 6.8556546 , 6.92820323, 7.07106781]]])
Coordinates:
  * time     (time) datetime64[ns] 16B 2024-06-01 2024-07-01
  * y        (y) int64 40B 500 400 300 200 100
  * x        (x) int64 40B 10 20 30 40 50
Attributes:
    units:        degrees C
    description:  Monthly mean temperature

Where Xarray really differs from NumPy is in summarizing data along dimensions. With NumPy, you must provide the axis number. With Xarray, you provide the dimension name. In the first example below, both lines calculate the mean temperature in each cell across the two months. The Xarray version is easier to interpret, and the coordinates for the remaining dimensions are maintained. In the second example, we calculate the maximum temperature in each month by summarizing over both spatial dimensions.

print(arr12.mean(axis=0))
[[12.5 13.5 14.5 15.5 16.5]
 [17.5 18.5 19.5 20.5 21.5]
 [22.5 23.5 24.5 25.5 26.5]
 [27.5 28.5 29.5 30.5 31.5]
 [32.5 33.5 34.5 35.5 37. ]]
print(da12.mean(dim="time"))
<xarray.DataArray 'temp' (y: 5, x: 5)> Size: 200B
array([[12.5, 13.5, 14.5, 15.5, 16.5],
       [17.5, 18.5, 19.5, 20.5, 21.5],
       [22.5, 23.5, 24.5, 25.5, 26.5],
       [27.5, 28.5, 29.5, 30.5, 31.5],
       [32.5, 33.5, 34.5, 35.5, 37. ]])
Coordinates:
  * y        (y) int64 40B 500 400 300 200 100
  * x        (x) int64 40B 10 20 30 40 50
Attributes:
    units:        degrees C
    description:  Monthly mean temperature
print(da12.max(dim=["y", "x"]))
<xarray.DataArray 'temp' (time: 2)> Size: 16B
array([24, 50])
Coordinates:
  * time     (time) datetime64[ns] 16B 2024-06-01 2024-07-01
Attributes:
    units:        degrees C
    description:  Monthly mean temperature

3.10.5 Comparisons and Masking

Comparison operators work the same as in NumPy and return a Boolean DataArray. To demonstrate this, we reuse the random array of integers from the Boolean Arrays section above. The where() method can then be used to mask values: values that meet the condition are maintained while all other values are replaced with NaN. Note that the result is converted to the float data type since integers cannot store NaN. A second argument can be provided to where() to replace values that do not meet the condition with a specific value as opposed to NaN.

arr10 = np.random.randint(1, 1200, 100)
arr10b = arr10.reshape(10, 10)
da10 = xr.DataArray(arr10b, dims=("y", "x"))

print(da10 > 150)
<xarray.DataArray (y: 10, x: 10)> Size: 100B
array([[ True,  True,  True,  True,  True,  True, False,  True,  True,
         True],
       [ True,  True,  True,  True,  True,  True,  True, False,  True,
         True],
       [ True,  True,  True,  True,  True,  True,  True,  True,  True,
         True],
       [ True,  True,  True, False,  True,  True, False,  True,  True,
         True],
       [ True,  True,  True,  True,  True, False,  True,  True,  True,
         True],
       [ True,  True,  True,  True, False,  True,  True,  True,  True,
         True],
       [ True,  True,  True,  True,  True,  True,  True,  True,  True,
         True],
       [ True,  True,  True,  True,  True,  True,  True,  True,  True,
         True],
       [ True,  True,  True,  True,  True,  True,  True,  True,  True,
         True],
       [ True,  True,  True,  True,  True,  True,  True,  True,  True,
         True]])
Dimensions without coordinates: y, x
print(da10.where(da10 > 150))
<xarray.DataArray (y: 10, x: 10)> Size: 800B
array([[ 490.,  438., 1191.,  278.,  228.,  300.,   nan,  390.,  711.,
        1035.],
       [ 883.,  519.,  849.,  927.,  901.,  164.,  462.,   nan,  515.,
         484.],
       [ 435.,  382., 1070.,  267.,  677.,  256.,  313.,  821.,  811.,
         463.],
       [ 969., 1159.,  813.,   nan,  344.,  763.,   nan, 1193.,  637.,
         947.],
       [ 913., 1034.,  555.,  954., 1138.,   nan,  246.,  429.,  818.,
         698.],
       [ 592.,  350.,  876.,  607.,   nan,  720., 1120.,  664.,  583.,
         361.],
       [ 235.,  920., 1141., 1104., 1072.,  409., 1083.,  835.,  341.,
         576.],
       [ 711.,  720.,  361.,  895., 1097.,  903.,  642.,  362.,  359.,
         614.],
       [ 324.,  267.,  195., 1077.,  219., 1168.,  924.,  973.,  407.,
         199.],
       [1178.,  282.,  185.,  665.,  730., 1186.,  240., 1180.,  295.,
         848.]])
Dimensions without coordinates: y, x
print(da10.where(da10 > 150, 0))
<xarray.DataArray (y: 10, x: 10)> Size: 400B
array([[ 490,  438, 1191,  278,  228,  300,    0,  390,  711, 1035],
       [ 883,  519,  849,  927,  901,  164,  462,    0,  515,  484],
       [ 435,  382, 1070,  267,  677,  256,  313,  821,  811,  463],
       [ 969, 1159,  813,    0,  344,  763,    0, 1193,  637,  947],
       [ 913, 1034,  555,  954, 1138,    0,  246,  429,  818,  698],
       [ 592,  350,  876,  607,    0,  720, 1120,  664,  583,  361],
       [ 235,  920, 1141, 1104, 1072,  409, 1083,  835,  341,  576],
       [ 711,  720,  361,  895, 1097,  903,  642,  362,  359,  614],
       [ 324,  267,  195, 1077,  219, 1168,  924,  973,  407,  199],
       [1178,  282,  185,  665,  730, 1186,  240, 1180,  295,  848]],
      dtype=int32)
Dimensions without coordinates: y, x

3.10.6 Broadcasting by Dimension Name

Above, we multiplied an array of shape (6, 6) by an array of shape (6). Following the NumPy broadcasting rules, the second array was treated as a single row and applied to every row (in other words, each column was multiplied by a different value). What if you instead wanted to multiply each row by a different value? With NumPy, you would need to reshape the second array to (6, 1). With Xarray, you simply assign the second array to the appropriate dimension name.

In the example, we use the values 1 through 6 as opposed to 2 so that the result is easier to interpret. When the multiplier is assigned to the “x” dimension, each column is multiplied by a different value. When it is assigned to the “y” dimension, each row is multiplied by a different value. The NumPy equivalent is provided for comparison.

arr1 = np.random.randint(1, 100, 36)
arr1b = arr1.reshape(6, 6)
mult = np.array([1, 2, 3, 4, 5, 6])

da1 = xr.DataArray(arr1b, dims=("y", "x"))
mult_x = xr.DataArray(mult, dims="x")
mult_y = xr.DataArray(mult, dims="y")

print(da1.values)
[[49 20 97 18 20  1]
 [ 4 70 58 50 91 53]
 [15 98 19 33 54 68]
 [89 13 37  5 22 58]
 [98 95 32 38 43 47]
 [29 87 94 12 82 22]]
print((da1 * mult_x).values)
[[ 49  40 291  72 100   6]
 [  4 140 174 200 455 318]
 [ 15 196  57 132 270 408]
 [ 89  26 111  20 110 348]
 [ 98 190  96 152 215 282]
 [ 29 174 282  48 410 132]]
print((da1 * mult_y).values)
[[ 49  20  97  18  20   1]
 [  8 140 116 100 182 106]
 [ 45 294  57  99 162 204]
 [356  52 148  20  88 232]
 [490 475 160 190 215 235]
 [174 522 564  72 492 132]]
print(np.array_equal(da1 * mult_y, arr1b * mult.reshape(6, 1)))
True

If two arrays have different dimension names, Xarray combines them into an array with all of the dimensions. For example, multiplying a DataArray with a “y” dimension by a DataArray with an “x” dimension results in a two-dimensional array.

print((mult_y * mult_x).dims)
('y', 'x')
print((mult_y * mult_x).values)
[[ 1  2  3  4  5  6]
 [ 2  4  6  8 10 12]
 [ 3  6  9 12 15 18]
 [ 4  8 12 16 20 24]
 [ 5 10 15 20 25 30]
 [ 6 12 18 24 30 36]]

3.10.7 Datasets

A Dataset stores multiple DataArrays that share dimensions and coordinates. In the example, we combine the temperature grids in degrees Celsius and degrees Fahrenheit into a single Dataset using a dictionary in which the keys are the variable names. Variables can then be accessed using bracket or dot notation, and selections and summaries can be applied to all variables at once.

ds = xr.Dataset({"temp_c": da12, "temp_f": da12_f})
print(ds)
<xarray.Dataset> Size: 896B
Dimensions:  (time: 2, y: 5, x: 5)
Coordinates:
  * time     (time) datetime64[ns] 16B 2024-06-01 2024-07-01
  * y        (y) int64 40B 500 400 300 200 100
  * x        (x) int64 40B 10 20 30 40 50
Data variables:
    temp_c   (time, y, x) int64 400B 0 1 2 3 4 5 6 7 ... 42 43 44 45 46 47 48 50
    temp_f   (time, y, x) float64 400B 32.0 33.8 35.6 37.4 ... 116.6 118.4 122.0
print(ds["temp_f"].sel(time="2024-07-01"))
<xarray.DataArray 'temp_f' (y: 5, x: 5)> Size: 200B
array([[ 77. ,  78.8,  80.6,  82.4,  84.2],
       [ 86. ,  87.8,  89.6,  91.4,  93.2],
       [ 95. ,  96.8,  98.6, 100.4, 102.2],
       [104. , 105.8, 107.6, 109.4, 111.2],
       [113. , 114.8, 116.6, 118.4, 122. ]])
Coordinates:
  * y        (y) int64 40B 500 400 300 200 100
  * x        (x) int64 40B 10 20 30 40 50
    time     datetime64[ns] 8B 2024-07-01
Attributes:
    units:        degrees F
    description:  Monthly mean temperature
print(ds.mean(dim=["y", "x"]))
<xarray.Dataset> Size: 48B
Dimensions:  (time: 2)
Coordinates:
  * time     (time) datetime64[ns] 16B 2024-06-01 2024-07-01
Data variables:
    temp_c   (time) float64 16B 12.0 37.04
    temp_f   (time) float64 16B 53.6 98.67

3.10.8 Converting Between NumPy, Xarray, and Pandas

Since many libraries expect NumPy arrays, it is common to convert between the two. The values property or to_numpy() method returns the data as a NumPy array, and a NumPy array can be converted to a DataArray using DataArray(), as demonstrated above. A DataArray can also be converted to a Pandas DataFrame, which is discussed in Chapter 4, using to_dataframe(). Each cell becomes a row, and the coordinates become the index.

Similar to NumPy, setting a variable equal to a DataArray will reference the same object in memory. Use the copy() method to create an independent copy.

arr12_back = da12.to_numpy()
print(type(arr12_back))
<class 'numpy.ndarray'>
print(arr12_back.shape)
(2, 5, 5)
print(da12.to_dataframe().head(10))
                   temp
time       y   x       
2024-06-01 500 10     0
               20     1
               30     2
               40     3
               50     4
           400 10     5
               20     6
               30     7
               40     8
               50     9

da12_copy = da12.copy()

3.10.9 Xarray and Geospatial Data

Xarray is commonly used to work with geospatial raster data and Earth science data. The open_dataset() function reads NetCDF and other supported files as a Dataset. The rioxarray extension, which is not included in the pybook environment, adds a rio accessor for reading and writing geospatial raster formats, such as GeoTIFF, and for working with coordinate reference systems. A multiband image read using rioxarray is returned as a DataArray with “band”, “y”, and “x” dimensions and the map coordinates of each cell.

The code below is provided as an example only and is not executed.

import rioxarray

img = rioxarray.open_rasterio("datasets/my_image.tif")
print(img.dims)
print(img.rio.crs)
red = img.sel(band=3)
nir = img.sel(band=4)
ndvi = (nir - red)/(nir + red)

3.11 Concluding Remarks

As mentioned above, NumPy is central to using Python for analyzing data. So, an understanding of NumPy is important for data scientists and geospatial data scientists. Additional libraries make use of NumPy to expand Python’s data science functionality. Xarray, which we explored in this chapter, is one example: it adds dimension names, coordinates, and attributes to NumPy arrays to simplify working with labeled, multidimensional data. In Chapter 4, we explore another library that builds on NumPy: Pandas.

3.12 Questions

  1. Explain two ways in which a NumPy array differs from a Python list.
  2. Explain the difference between signed and unsigned integer data types. Why might an 8-bit unsigned integer (uint8) data type be appropriate for some image data?
  3. For a three-dimensional array representing a multispectral image, explain what the shape, ndim, and size properties describe.
  4. Explain the purpose of the reshape() method. What does providing -1 for a dimension accomplish?
  5. Explain the difference between a copy and a view of an array. Why does this difference matter?
  6. What is broadcasting? Give an example of an operation between two arrays with different shapes that is made possible by broadcasting.
  7. How does Xarray represent the dimensions of an array differently than NumPy? What is one benefit of this approach for geospatial data?
  8. Explain the difference between the isel() and sel() methods in Xarray, including how they differ when slicing.
  9. Explain the difference between a DataArray and a Dataset.