Python High Performance Programming — Save 50%
Boost the performance of your Python programs using advanced techniques with this book and ebook
NumPy is the de facto standard for scientific computing in Python. It extends Python with a flexible multidimensional array that allows fast mathematical calculations.
NumPy works as a framework that allows coding complex operations using a concise syntax. The multidimensional array (numpy.ndarray) is internally based on C arrays: in this way the developer can easily interface NumPy with existing C and FORTRAN code. NumPy constitutes a bridge between Python and the legacy code written using those languages.
In this article, by Gabriele Lanaro, author of Python High Performance Programming, we will learn how to create and access NumPy arrays.
In the last few years a number of packages were developed to further increase the speed of NumPy. We will explore one of these packages, numexpr, that optimizes array expressions and takes advantage of multi-core architectures.
(For more resources related to this topic, see here.)
Getting started with NumPy
NumPy is founded around its multidimensional array object, numpy.ndarray. NumPy arrays are a collection of elements of the same data type; this fundamental restriction allows NumPy to pack the data in an efficient way. By storing the data in this way NumPy can handle arithmetic and mathematical operations at high speed.
You can create NumPy arrays using the numpy.array function. It takes list-like object (or another array) as input and, optionally, a string expressing its data type. You can interactively test array creation using an IPython shell as follows:
In : import numpy as np In : a = np.array([0, 1, 2])
Every NumPy array has a data type that can be accessed by the dtype attribute, as shown in the following code. In the following code example, dtype is a 64-bit integer.
In : a.dtype Out: dtype('int64')
If we want those numbers to be treated as a float type of variable, we can either pass the dtype argument in the np.array function or cast the array to another data type using the astype method as shown in the following code:
In : a = np.array([1, 2, 3], dtype='float32') In : a.astype('float32') Out: array([ 0., 1., 2.], dtype=float32)
To create an array with two dimensions (an array of arrays) we can initialize the array using a nested sequence shown as follows:
In : a = np.array([[0, 1, 2], [3, 4, 5]]) In : print(a) Out: [[0 1 2]
[3 4 5]]
The array created in this way has two dimensions—axes in NumPy's jargon. Such an array is like a table that contains two rows and three columns. We can access the axes structure using the ndarray.shape attribute:
In : a.shape Out: (2, 3)
Arrays can also be reshaped only as long as the product of the shape dimensions is equal to the total number of elements in the array. For example, we can reshape an array containing 16 elements in the following ways: (2, 8), (4, 4), or (2, 2, 4). To reshape an array we can either use the ndarray.reshape method or directly change the ndarray.shape attribute. The following code illustrates the use of the ndarray.reshape method:
In : a = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8,
9, 10, 11, 12, 13, 14, 15]) In : a.shape Out: (16,) In : a.reshape(4, 4) # Equivalent: a.shape = (4, 4) Out: array([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11],
[12, 13, 14, 15]])
Thanks to this property you are also free to add dimensions of size one. You can reshape an array with 16 elements to (16, 1), (1, 16), (16, 1, 1), and so on.
NumPy provides convenience functions, shown in the following code, to create arrays filled with zeros, filled with ones, or without an initialization value (empty—their actual value is meaningless and depends on the memory state). Those functions take the array shape as a tuple and optionally its dtype.
In : np.zeros((3, 3)) In : np.empty((3, 3)) In : np.ones((3, 3), dtype='float32')
In our examples we will use the numpy.random module to generate random floating point numbers in the (0, 1) interval. The numpy.random module is shown as follows: In : np.random.rand(3, 3)
Sometimes it is convenient to initialize arrays that have a similar shape to other arrays. Again, NumPy provides some handy functions for that purpose such as zeros_like, empty_like, and ones_like. These functions are as follows:
In : np.zeros_like(a) In : np.empty_like(a) In : np.ones_like(a)
NumPy array interface is, on a shallow level, similar to Python lists. They can be indexed using integers, and can also be iterated using a for loop.
In : A = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8]) In : A Out: 0 In : [a for a in A] Out: [0, 1, 2, 3, 4, 5, 6, 7, 8]
It is also possible to index an array in multiple dimensions. If we take a (3,3) array (an array containing 3 triplets) and we index the first element, we obtain the first triplet shown as follows:
In : A = np.array([[0, 1, 2], [3, 4, 5], [6, 7, 8]]) In : A Out: array([0, 1, 2])
We can index the triplet again by adding the other index separated by a comma. To get the second element of the first triplet we can index using [0, 1] as shown in the following code:
In : A[0, 1] Out: 1
NumPy allows you to slice arrays in single and multiple dimensions. If we index on the first dimension we will get a collection of triplets shown as follows:
In : A[0:2] Out: array([[0, 1, 2], [3, 4, 5]])
If we slice the array with [0:2]. for every selected triplet we extract the first two elements, resulting in a (2, 2) array shown in the following code:
In : A[0:2, 0:2] Out: array([[0, 1], [3, 4]])
Intuitively, you can update values in the array by using both numerical indexes and slices. The syntax is as follows:
In : A[0, 1] = 8 In : A[0:2, 0:2] = [[1, 1], [1, 1]]
Indexing with the slicing syntax is fast because it doesn't make copies of the array. In NumPy terminology it returns a view over the same memory area. If we take a slice of the original array and then changes one of its value; the original array will be updated as well. The following code illustrates an example of the same:
In : a = np.array([1, 1, 1, 1]) In : a_view = A[0:2] In : a_view = 2 In : print(A) Out: [2 1 1 1]
We can take a look at another example that shows how the slicing syntax can be used in a real-world scenario. We define an array r_i, shown in the following line of code, which contains a set of 10 coordinates (x, y); its shape will be (10, 2):
In : r_i = np.random.rand(10, 2)
A typical operation is extracting the x component of each coordinate. In other words you want to extract the items [0, 0], [1, 0], [2, 0], and so on. resulting in an array with shape (10,). It is helpful to think that the first index is moving while the second one is fixed (at 0). With this in mind, we will slice every index on the first axis (the moving one) and take the first element (the fixed one) on the second axis as shown in the following line of code:
In : x_i = r_i[:, 0]
On the other hand, the following expression of code will keep the first index fixed and the second index moving, giving the first (x, y) coordinate:
In : r_0 = r_i[0, :]
Slicing all the indexes over the last axis is optional; using r_i has the same effect as r_i[0, :].
NumPy allows to index an array by using another NumPy array made of either integer or Boolean values—a feature called fancy indexing.
If you index with an array of integers, NumPy will interpret the integers as indexes and will return an array containing their corresponding values. If we index an array containing 10 elements with [0, 2, 3], we obtain an array of size 3 containing the elements at positions 0, 2 and 3. The following code gives us an illustration of this concept:
In : a = np.array([9, 8, 7, 6, 5, 4, 3, 2, 1, 0]) In : idx = np.array([0, 2, 3]) In : a[idx] Out: array([9, 7, 6])
You can use fancy indexing on multiple dimensions by passing an array for each dimension. If we want to extract the elements [0, 2] and [1, 3] we have to pack all the indexes acting on the first axis in one array, and the ones acting on the second axis in another. This can be seen in the following code:
In : a = np.array([[0, 1, 2], [3, 4, 5], [6, 7, 8], [9, 10, 11]]) In : idx1 = np.array([0, 1]) In : idx2 = np.array([2, 3]) In : a[idx1, idx2]
You can also use normal lists as index arrays, but not tuples. For example the following two statements are equivalent:
>>> a[np.array([0, 1])] # is equivalent to >>> a[[0, 1]]
However, if you use a tuple, NumPy will interpret the following statement as an index on multiple dimensions:
>>> a[(0, 1)] # is equivalent to
>>> a[0, 1]
The index arrays are not required to be one-dimensional; we can extract elements from the original array in any shape. For example we can select elements from the original array to form a (2,2) array shown as follows:
In : idx1 = [[0, 1], [3, 2]] In : idx2 = [[0, 2], [1, 1]] In : a[idx1, idx2] Out: array([[ 0, 5],
The array slicing and fancy indexing features can be combined. For example, this is useful if we want to swap the x and y columns in a coordinate array. In the following code, the first index will be running over all the elements (a slice), and for each of those we extract the element in position 1 (the y) first and then the one in position 0 (the x):
In : r_i = np.random(10, 2) In : r_i[:, [0, 1]] = r_i[:, [1, 0]]
When the index array is a Boolean there are slightly different rules. The Boolean array will act like a mask; every element corresponding to True will be extracted and put in the output array. This procedure is shown as follows:
In : a = np.array([0, 1, 2, 3, 4, 5]) In : mask = np.array([True, False, True, False, False, False]) In : a[mask] Out: array([0, 2])
The same rules apply when dealing with multiple dimensions. Furthermore, if the index array has the same shape as the original array, the elements corresponding to True will be selected and put in the resulting array.
Indexing in NumPy is a reasonably fast operation. Anyway, when speed is critical, you can use the, slightly faster, numpy.take and numpy.compress functions to squeeze out a little more speed. The first argument of numpy.take is the array we want to operate on, and the second is the list of indexes we want to extract. The last argument is axis; if not provided, the indexes will act on the flattened array, otherwise they will act along the specified axis.
In : r_i = np.random(100, 2) In : idx = np.arange(50) # integers 0 to 50 In : %timeit np.take(r_i, idx, axis=0) 1000000 loops, best of 3: 962 ns per loop In : %timeit r_i[idx] 100000 loops, best of 3: 3.09 us per loop
The similar, but faster version for Boolean arrays is numpy.compress which works in the same way. The use of numpy.compress is shown as follows:
In : idx = np.ones(100, dtype='bool') # all True values In : %timeit np.compress(idx, r_i, axis=0) 1000000 loops, best of 3: 1.65 us per loop In : %timeit r_i[idx] 100000 loops, best of 3: 5.47 us per loop
The article thus covers the basics of NumPy arrays, talking about the creating of arrays and how we can access them.
Resources for Article:
- Getting Started with Spring Python [Article]
- Python Testing: Installing the Robot Framework [Article]
- Python Multimedia: Fun with Animations using Pyglet [Article]
|Boost the performance of your Python programs using advanced techniques with this book and ebook|
eBook Price: $17.99
Book Price: $29.99
About the Author :
Gabriele Lanaro is a PhD student in Chemistry at the University of British Columbia, in the field of Molecular Simulation. He writes high performance Python code to analyze chemical systems in large-scale simulations. He is the creator of Chemlab—a high performance visualization software in Python—and emacs-for-python—a collection of emacs extensions that facilitate working with Python code in the emacs text editor. This book builds on his experience in writing scientific Python code for his research and personal projects.