*LearnNumPy fast operation and computations in this article by Alberto Boschetti, a data scientist with expertise in signal processing and statistics and Luca Massaron, a data scientist and marketing research director specialized in multivariate statistical analysis, machine learning, and customer insight.*

When arrays need to be manipulated by mathematical operations, you just need to apply the operation on the array with respect to a numerical constant (a scalar) or an array of the same shape:

1 2 3 4 5 6 |
In: import numpy as np a = np.arange(5).reshape(1,5) a += 1 a*a Out: array([[ 1, 4, 9, 16, 25]]) |

As a result, the operation is to be performed element-wise; that is, every element of the array is operated by either the scalar value or the corresponding element of the other array.

When operating on arrays of different dimensions, it is still possible to obtain element-wise operations without having to restructure the data if one of the corresponding dimensions is 1. In fact, in such a case, the dimension of size 1 is stretched until it matches the dimension of the corresponding array. This conversion is called broadcasting. For instance:

1 2 3 4 5 6 7 8 9 |
In: a = np.arange(5).reshape(1,5) + 1 b = np.arange(5).reshape(5,1) + 1 a * b Out: array([[ 1, 2, 3, 4, 5], [ 2, 4, 6, 8, 10], [ 3, 6, 9, 12, 15], [ 4, 8, 12, 16, 20], [ 5, 10, 15, 20, 25]]) |

However, it won’t require an expansion of memory of the original arrays in order to obtain pair-wise multiplication.

Furthermore, there exists a wide range of NumPy functions that can operate element-wise on arrays: abs() , sign() , round() , floor(), sqrt(), log(), and exp() .

Other usual operations that could be operated by NumPy functions are sum() and prod(), which provide the summation and product of the array rows or columns on the basis of the specified axis:

1 2 3 4 5 6 7 8 9 10 11 12 |
In: print (a2) Out: [[1 2 3 4 5] [1 2 3 4 5] [1 2 3 4 5] [1 2 3 4 5] [1 2 3 4 5]] In: np.sum(a2, axis=0) Out: array([ 5, 10, 15, 20, 25]) In: np.sum(a2, axis=1) Out: array([15, 15, 15, 15, 15]) |

When operating on your data, remember that operations and NumPy functions on arrays are extremely fast when compared to simple Python lists. Now, try out a couple of experiments. First, compare a list comprehension to an array when dealing with a sum of a constant:

1 2 3 4 5 6 |
In: %timeit -n 1 -r 3 [i+1.0 for i in range(10**6)] %timeit -n 1 -r 3 np.arange(10**6)+1.0 Out: 1 loops, best of 3: 158 ms per loop 1 loops, best of 3: 6.64 ms per loop |

On Jupyter, %time allows you to easily benchmark operations. Then, the -n 1 parameter requires the benchmark to execute the code snippet for only one loop; -r 3 requires you to retry the execution of the loops (in this case, just one loop) three times and report the best performance recorded from such repetitions.

Results on your computer may vary depending on your configuration and operating system. Anyway, the difference between the standard Python operations and the NumPy ones will remain quite large. Though unnoticeable when working on small datasets, this difference can really impact your analysis when dealing with larger data or when looping over and over the same analysis pipeline for parameter or variable selection.

This also happens when applying sophisticated operations, such as finding a square root:

1 2 3 4 5 6 7 |
In: import math %timeit -n 1 -r 3 [math.sqrt(i) for i in range(10**6)] Out: 1 loops, best of 3: 222 ms per loop In: %timeit -n 1 -r 3 np.sqrt(np.arange(10**6)) Out: 1 loops, best of 3: 6.9 ms per loop |

Sometimes, you may need to apply custom functions to your array instead. The apply_along_axis function lets you use a custom function and apply it to an axis of an array:

1 2 3 4 5 6 7 8 9 10 |
In: defcube_power_square_root(x): returnnp.sqrt(np.power(x, 3)) np.apply_along_axis(cube_power_square_root, axis=0, arr=a2) Out: array([[ 1., 2.82842712, 5.19615242, 8., 11.18033989], [ 1., 2.82842712, 5.19615242, 8., 11.18033989], [ 1., 2.82842712, 5.19615242, 8., 11.18033989], [ 1., 2.82842712, 5.19615242, 8., 11.18033989], [ 1., 2.82842712, 5.19615242, 8., 11.18033989]]) |

### Matrix operations

Apart from element-wise calculations using the np.dot() function, you can also apply multiplications to your two-dimensional arrays based on matrix calculations, such as vector-matrix and matrix-matrix multiplications:

1 2 3 4 5 6 7 8 9 |
In: import numpy as np M = np.arange(5*5, dtype=float).reshape(5,5) M Out: 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.]]) |

As an example, create a 5 x 5 two-dimensional array of ordinal numbers from 0 to 24:

- Define a vector of coefficients and an array column stacking the vector and its reverse:

1 2 3 4 5 6 7 8 9 |
In: coefs = np.array([1., 0.5, 0.5, 0.5, 0.5]) coefs_matrix = np.column_stack((coefs,coefs[::-1])) print (coefs_matrix) Out:[[ 1. 0.5] [ 0.5 0.5] [ 0.5 0.5] [ 0.5 0.5] [ 0.5 1. ]] |

- Now, multiply the array with the vector using the dotfunction:

1 2 |
In: np.dot(M,coefs) Out: array([ 5., 20., 35., 50., 65.]) |

- Alternatively, the vector by the array:

1 2 |
In: np.dot(coefs,M) Out: array([ 25., 28., 31., 34., 37.]) |

- Or the array by the stacked coefficient vectors (which is a 5 x 2 matrix):

1 2 3 4 5 6 7 |
In: np.dot(M,coefs_matrix) Out: array([[ 5., 7.], [ 20., 22.], [ 35., 37.], [ 50., 52.], [ 65., 67.]]) |

NumPy also offers an object class, matrix, which is actually a subclass of ndarray , inheriting all its attributes and methods. NumPy matrices are exclusively two-dimensional (as arrays are actually multi-dimensional) by default. When multiplied, they apply matrix products, not element-wise ones (the same happens when raising powers) and they have some special matrix methods ( .H for the conjugate transpose and .I for the inverse).

Apart from the convenience of operating in a fashion that is similar to that of MATLAB, they do not offer any other advantage. You may risk confusion in your scripts since you’ll have to handle different product notations for matrix objects and arrays.

### Slicing and indexing with NumPy arrays

Indexing allows you to take a view of a ndarray by pointing out either what slice of columns and rows to visualize or an index:

- Define a working array:

1 2 |
In: import numpy as np M = np.arange(10*10, dtype=int).reshape(10,10) |

- Your array is a 10 x 10 two-dimensional array. Start by slicing it into a single dimension. The notation for a single dimension is the same as that in Python lists:

1 |
[start_index_included:end_index_exclude:steps] |

- You may want to extract even rows from 2 to 8:

1 2 3 4 5 |
In: M[2:9:2,:] Out: array([[20, 21, 22, 23, 24, 25, 26, 27, 28, 29], [40, 41, 42, 43, 44, 45, 46, 47, 48, 49], [60, 61, 62, 63, 64, 65, 66, 67, 68, 69], [80, 81, 82, 83, 84, 85, 86, 87, 88, 89]]) |

- After slicing the rows, slice the columns even further by taking only the columns from index 5:

1 2 3 4 5 |
In: M[2:9:2,5:] Out: array([[25, 26, 27, 28, 29], [45, 46, 47, 48, 49], [65, 66, 67, 68, 69], [85, 86, 87, 88, 89]]) |

- As in lists, it is possible to use negative index values in order to start counting from the end. Moreover, a negative number for parameters, such as steps, reverses the order of the output array, like in the following example, where the counting starts from column index
**5**but in the reverse order and goes toward index**0**:

1 2 3 4 5 6 |
In: M[2:9:2,5::-1] Out: array([[25, 24, 23, 22, 21, 20], [45, 44, 43, 42, 41, 40], [65, 64, 63, 62, 61, 60], [85, 84, 83, 82, 81, 80]]) |

- You can also create Boolean indexes that point out the rows and columns to select. Therefore, you can replicate the previous example using a row_indexand a col_index variable:

1 2 3 4 5 6 7 8 9 10 11 |
In: row_index = (M[:,0]>=20) & (M[:,0]<=80) col_index = M[0,:]>=5 M[row_index,:][:,col_index] Out:array([[25, 26, 27, 28, 29], [35, 36, 37, 38, 39], [45, 46, 47, 48, 49], [55, 56, 57, 58, 59], [65, 66, 67, 68, 69], [75, 76, 77, 78, 79], [85, 86, 87, 88, 89]]) |

You cannot contextually use Boolean indexes on both columns and rows in the same square brackets, though you can apply the usual indexing to the other dimension using integer indexes. Consequently, you have to first operate a Boolean selection on rows and then reopen the square brackets and operate a second selection on the first, this time focusing on the columns.

- If you need a global selection of elements in the array, you can also use a mask of Boolean values, as follows:

1 2 3 4 5 6 |
In: mask = (M>=20) & (M<=90) & ((M / 10.) % 1 >= 0.5) M[mask] Out: array([25, 26, 27, 28, 29, 35, 36, 37, 38, 39, 45, 46, 47, 48, 49, 55, 56, 57, 58, 59, 65, 66, 67, 68, 69, 75, 76, 77, 78, 79, 85, 86, 87, 88, 89]) |

This approach is particularly useful if you need to operate on the partition of the array selected by the mask (for example, M[mask]=0 ).

Another way to point out the elements that need to be selected from your array is by providing a row or column index consisting of integers. Such indexes may be defined either by a
np.where() function that transforms a Boolean condition on an array into indexes or by simply providing a sequence of integer indexes, where integers may be in a particular order or might even be repeated. Such an approach is called **fancy indexing**:

1 2 |
In: row_index = [1,1,2,7] col_index = [0,2,4,8] |

Having defined the indexes of your rows and columns, you have to apply them contextually to select elements whose coordinates are given by the tuple of values of both the indexes:

1 2 |
In: M[row_index,col_index] Out: array([10, 12, 24, 78]) |

In this way, the selection will report the following points: **(1,0)**, **(1,2)**, **(2,4)**, and **(7,8)**. Otherwise, you have to select the rows first and then the columns, which are separated by square brackets:

1 2 3 4 5 6 |
In: M[row_index,:][:,col_index] Out: array([[10, 12, 14, 18], [10, 12, 14, 18], [20, 22, 24, 28], [70, 72, 74, 78]]) |

Finally, remember that slicing and indexing are just views of the data. If you need to create new data from such views, you have to use the .copy method on the slice and assign it to another variable. Otherwise, any modification to the original array will be reflected on your slice and vice versa. The copy method is shown here:

1 |
In: N = M[2:9:2,5:].copy() |

*If you found this article interesting, you can explore **Python Data Science Essentials** to gain useful insights from your data using popular data science tools. Fully expanded and upgraded, the latest edition of **Python Data Science Essentials** offers up-to-date insight into the core of Python, including the latest versions of the Jupyter Notebook, NumPy, pandas, and scikit-learn.*