# NumPy

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 number of dimensions while Pandas allows for reading in and working with data tables. In this section, we will focus on NumPy.

After working through this module you will be able to:

1. describe and use NumPy data types.
2. describe the data type, size, shape, and number of dimensions in an array.
3. create, reshape, and slice NumPy arrays.
4. perform numeric and comparison operations on arrays.

## 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. Also, it is very fast and memory efficient due to its reliance on the C language. As mentioned in the prior modules, 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 3 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 Anaconda environment, as demonstrated in the set-up module. 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 name of "np" to simplify your code.

``````import numpy as np
``````

Lists can be converted to NumPy arrays using the array() method. 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))
print(type(arr1))
print(arr1)
``````
``````<class 'list'>
<class 'numpy.ndarray'>
[3 6 7 8 9]
``````

A two dimensional array is known as a matrix. In the example below, I am generating a matrix array 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, I am creating a three dimensional array from a list of lists of lists. This would be similar to an image with dimensions 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 methods for generating arrays.

The arange() method returns an array of evenly spaced values and accepts start, stop, step, and data type parameters. In the example, I have created an array of evenly spaced values from 0 to 100 with a step size of 5. I specifically define the data type as integer, but NumPy can infer a data type if it is not provided.

The linspace() method 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() method is used to return an array of 1s. In the example, I have generated 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, 100, 5, dtype="int")
print(arr4)

arr5 = np.linspace(0, 100, 5, dtype="int")
print(arr5)

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

arr7 = np.zeros((3, 4, 2))
print(arr7)

arr8 = np.random.rand(3, 4, 5)
print(arr8)

arr9 = np.random.randint(1, 200, 7)
print(arr9)
``````
``````[ 0  5 10 15 20 25 30 35 40 45 50 55 60 65 70 75 80 85 90 95]
[  0  25  50  75 100]
[[[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.]]]
[[[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.61611044 0.82997297 0.95698234 0.63758879 0.52442317]
[0.26018298 0.94560291 0.87566749 0.77677722 0.07518612]
[0.1291478  0.27554435 0.49308256 0.06694469 0.37334858]
[0.67701685 0.62236129 0.32886104 0.14403291 0.38487002]]

[[0.94066151 0.82999355 0.67013645 0.6089111  0.76797975]
[0.08925162 0.70730738 0.38616466 0.99214843 0.99653095]
[0.42314746 0.50925596 0.85850569 0.26249533 0.4433054 ]
[0.01790036 0.82842809 0.31873158 0.46986309 0.37309847]]

[[0.73009527 0.74129696 0.36985928 0.66016071 0.19166952]
[0.70313963 0.33993884 0.80155675 0.55717499 0.50198088]
[0.94621494 0.29111301 0.91356446 0.40601907 0.27936317]
[0.29871494 0.42579042 0.76071243 0.14393261 0.63504426]]]
[153  77 161 175 192 174   6]
``````

## NumPy Data Types

NumPy provides additional and more specific data types in comparison to base Python. Here, I 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 will not discuss here.

Below I have demonstrated how to define the data type with the dtype parameter. In all cases I am using .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. Note that the data type will impact the amount of memory needed. For example an int8 will require less memory than an int16. I 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)
print(arr1.dtype)

arr1 = np.ones((3), dtype="int16")
print(arr1)
print(arr1.dtype)

arr1 = np.ones((3), dtype="float16")
print(arr1)
print(arr1.dtype)

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

## 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 will return 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 features in the array. There are 48 features 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)))
print("Shape of array: " + str(arr6.shape))
print("Number of dimensions: " + str(arr6.ndim))
print("Size of array: " + str(arr6.size))
print("Data type of array: " + str(arr6.dtype))
``````
``````Length of first dimension: 3
Shape of array: (3, 4, 4)
Number of dimensions: 3
Size of array: 48
Data type of array: float64
``````

NumPy has a built-in methods for changing the shape of an array, .reshape(). Note that the number of features or size of the array must perfectly fill the new shape. In the first example, I am maintaining the number of dimensions but changing the shape or length of each dimension. In the second two examples, I am converting the three-dimensional array to two-dimensional arrays. Lastly, I 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)
print(arr6c)
print(arr6d)
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.]]]
[[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.]]
[[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.]]
[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 I am converting a vector array into a multidimensional array or matrix.

``````arr10 = np.random.randint(1, 1200, 100)
arr10b = arr10.reshape(10, 10)
print(arr10b)
``````
``````[[ 472 1139  637  971  282  194  395   30  370  869]
[ 635  343  218  440  538  651 1146  269 1141  428]
[1163  261  691  366  505  784  354  799  381 1042]
[1058 1078  226  374 1088   83  881  949 1010 1046]
[ 929  944  369   67  806  910  422  841  857  152]
[ 587  513 1039  680   23  500  769  338  627 1075]
[ 418  153 1067  515 1148  553  651 1145 1102  568]
[ 624  321  401 1013  810 1130  588  122  893  638]
[   2 1149  977  459  115  347  956  786  992  187]
[ 496  997  288  556   83  932  298  721  663  166]]
``````

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)
print(arr10b.shape)
print(arr10c.shape)
print(arr10d.shape)
``````
``````(1000,)
(10, 10, 10)
(10, 10, 10)
(10, 10, 10)
``````

## 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")
print(arr12)
print("All values in first index of first dimension")
print(arr12) #This will extract just the values from the first index in the first dimension
print("All values in second index of first dimension")
print(arr12) #This will extract just the values from the second index in the first dimension
print("All values in first index of first dimension and first index of second dimension")
print(arr12) #This will extract all values occurring in the first index of both the first and second dimensions
print("A single value specified with three indexes, one for reach dimension")
print(arr12[1, 3, 3]) #This will extract a specific value based on an index in all three dimensions
print("Incorporating ranges")
print(arr12[1, 0:2, 0:2]) #All values in second index of first dimension that are also include in the first to second index of the second and third dimensions
print("Using colons")
print(arr12[:, 0:2, 0:2]) #Only a colon means select all values in a dimension
print(arr12[:,2:,0:2]) #Can also use colons to select all values before or after an index
``````
``````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 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]]]
All values in first index of 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]]
All values in second index of 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]]
All values in first index of first dimension and first index of second dimension
[0 1 2 3 4]
A single value specified with three indexes, one for reach dimension
43
Incorporating ranges
[[25 26]
[30 31]]
Using colons
[[[ 0  1]
[ 5  6]]

[[25 26]
[30 31]]]
[[[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 I have converted all values in the first index of the first dimension and the first index of the second dimension to 0.

``````arr12 = 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]]]
``````

## 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)
arr10bool = arr10b > 150
print(arr10bool)
``````
``````[[ 167  986   83  247  886  993  237  326  931  127]
[ 902  401  823  297  111  948  330  888 1128  751]
[ 283  966  629 1182   53  126  189  316  810   17]
[ 997  403  438  121  205  613  317  836   18  837]
[ 410  177  651  432 1069 1176  909  396  665 1098]
[  30  919  446  317  733   83  407  429  430  192]
[ 557  239  100  234  146  620  507  398  942  261]
[ 779  250  712  172  196  685  455  313  198 1130]
[ 235  274  782  438  239  222  288 1173 1066  100]
[1004 1147  639  579  294   44   27  735  646  573]]
[[ True  True False  True  True  True  True  True  True False]
[ True  True  True  True False  True  True  True  True  True]
[ True  True  True  True False False  True  True  True False]
[ True  True  True False  True  True  True  True False  True]
[ True  True  True  True  True  True  True  True  True  True]
[False  True  True  True  True False  True  True  True  True]
[ True  True False  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 False]
[ True  True  True  True  True False False  True  True  True]]
``````

## Copy vs. View

In the first module, I explained how, for mutable data types, setting a variable equal to another variable will result 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. Thus, 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. Also using view(), you can 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 the original or copied object will not impact the original object.

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 impacts 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)

arr2 = arr1.view()
arr3 = arr1.copy()
arr1[:, :] = 0
print(arr1)
print(arr2)
print(arr3)
``````
``````[[89 12 56 15 87]
[11 34 87 41 75]
[67 66 56 53 70]
[90 80 88 73 16]
[ 9 16 98 61 44]]
[[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]]
[[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]]
[[89 12 56 15 87]
[11 34 87 41 75]
[67 66 56 53 70]
[90 80 88 73 16]
[ 9 16 98 61 44]]
``````
``````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)

arr2 = arr1.view()
arr3 = arr1.copy()
arr2[:, :] = 0
print(arr1)
print(arr2)
print(arr3)
``````
``````[[26 41 48 26  7]
[77 18 73 56 86]
[ 3 19 73 84 73]
[88 42 41 56 92]
[94 10 89 37 73]]
[[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]]
[[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]]
[[26 41 48 26  7]
[77 18 73 56 86]
[ 3 19 73 84 73]
[88 42 41 56 92]
[94 10 89 37 73]]
``````
``````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)

arr2 = arr1.view()
arr3 = arr1.copy()
arr3[:, :] = 0
print(arr1)
print(arr2)
print(arr3)
``````
``````[[64 86 52 37 99]
[ 4 85 54 51 93]
[52 50 89 67  6]
[98 54 77 48 56]
[75 93 76 60 81]]
[[64 86 52 37 99]
[ 4 85 54 51 93]
[52 50 89 67  6]
[98 54 77 48 56]
[75 93 76 60 81]]
[[64 86 52 37 99]
[ 4 85 54 51 93]
[52 50 89 67  6]
[98 54 77 48 56]
[75 93 76 60 81]]
[[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]]
``````

### 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)
print(arr14b+21)
print(arr14b-52)
print(arr14b*2)
print(arr14b/3)
print(arr14b**2)
``````
``````[[ 371  477  792  841  137]
[ 457 1137  986  766  172]
[ 561  652  389  789  850]
[ 886  971  322  979 1148]
[ 274   54  683   13  574]]
[[ 392  498  813  862  158]
[ 478 1158 1007  787  193]
[ 582  673  410  810  871]
[ 907  992  343 1000 1169]
[ 295   75  704   34  595]]
[[ 319  425  740  789   85]
[ 405 1085  934  714  120]
[ 509  600  337  737  798]
[ 834  919  270  927 1096]
[ 222    2  631  -39  522]]
[[ 742  954 1584 1682  274]
[ 914 2274 1972 1532  344]
[1122 1304  778 1578 1700]
[1772 1942  644 1958 2296]
[ 548  108 1366   26 1148]]
[[123.66666667 159.         264.         280.33333333  45.66666667]
[152.33333333 379.         328.66666667 255.33333333  57.33333333]
[187.         217.33333333 129.66666667 263.         283.33333333]
[295.33333333 323.66666667 107.33333333 326.33333333 382.66666667]
[ 91.33333333  18.         227.66666667   4.33333333 191.33333333]]
[[ 137641  227529  627264  707281   18769]
[ 208849 1292769  972196  586756   29584]
[ 314721  425104  151321  622521  722500]
[ 784996  942841  103684  958441 1317904]
[  75076    2916  466489     169  329476]]
``````

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)
print(arr14b+arr14b)
print(arr14b-arr14b)
``````
``````[[ 572  247  970  844  997]
[ 700  996  455  176  399]
[ 750  328  799  665  795]
[ 305  975  397  878  556]
[ 916  982 1106  157  391]]
[[1144  494 1940 1688 1994]
[1400 1992  910  352  798]
[1500  656 1598 1330 1590]
[ 610 1950  794 1756 1112]
[1832 1964 2212  314  782]]
[[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., 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 must be converted to 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 broadcasted 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)
print(arr2)
print(arr1b*arr2)
``````
``````[[ 8 17 66 68 36 65]
[75 46 62 86  8 89]
[81 87 89 88 22 24]
[44 59 88 72 90 69]
[47 70 85 31 30 37]
[ 9 77 48  5 83 68]]
[2. 2. 2. 2. 2. 2.]
[[ 16.  34. 132. 136.  72. 130.]
[150.  92. 124. 172.  16. 178.]
[162. 174. 178. 176.  44.  48.]
[ 88. 118. 176. 144. 180. 138.]
[ 94. 140. 170.  62.  60.  74.]
[ 18. 154.  96.  10. 166. 136.]]
``````

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

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

print(np.max(arr14b))
print(np.min(arr14b))
print(np.sqrt(arr14b))
``````
``````1135
15
[[21.84032967 30.28200786 16.73320053 21.11871208 27.64054992]
[24.8997992  21.04756518 26.98147513 33.12099032 33.36165464]
[22.53885534 32.17141588 24.67792536 31.96873473 33.68976106]
[ 3.87298335  7.          6.244998   17.97220076 24.41311123]
[21.54065923 29.69848481 10.39230485 13.07669683 27.80287755]]
``````

## Concluding Remarks

As mentioned above, NumPy is central to using Python for analyzing data. So, an understanding of NumPy is important for data and geospatial data scientists. Additional libraries and modules make use of NumPy to expand Python's data science functionalities. In the next section, we will explore one of these libraries: Pandas.