



Version 1.0 - January 2017

There is already a fair number of book about numpy (see) and a legitimate question is to wonder if another book is really necessary. As you may have guessed by reading these lines, my personal answer is yes, mostly because I think there's room for a different approach concentrating on the migration from python to numpy through vectorization. There is a lot of techniques that you don't find in books and such techniques are mostly learned through experience. The goal of this book is to explain some of them and to make you acquire experience in the process.
Website: http://www.labri.fr/perso/nrougier/from-python-to-numpy
Table of ContentsDisclaimer:All external pictures should have associated credits. If there are missing credits, please tell me, I will correct it. Similarly, all excerpts should be sourced (wikipedia mostly). If not, this is an error and I will correct it as soon as you tell me.
ContentsNicolas P. Rougier is a full-time research scientist at Inria which is the French national institute for research in computer science and control. This is a public scientific and technological establishment (EPST) under the double supervision of the Research & Education Ministry, and the Ministry of Economy Finance and Industry. Nicolas P. Rougier is working within the Mnemosyne project which lies at the frontier between integrative and computational neuroscience in association with the Institute of Neurodegenerative Diseases , the Bordeaux laboratory for research in computer science (LaBRI), the University of Bordeaux and the national center for scientific research ( CNRS ).
He has been using Python for more than 15 years and numpy for more than 10 years for modeling in neuroscience, machine learning and for advanced visualization (OpenGL). Nicolas P. Rougier is the author of several online resources and tutorials (Matplotlib, numpy, OpenGL) and he's teaching Python, numpy and scientific visualization at the University of Bordeaux and in various conferences and schools worldwide (SciPy, EuroScipy, etc). He's also the author of the popular article Ten Simple Rules for Better Figures and a popular 'matplotlib tutorial < http://www.labri.fr/perso/nrougier/teaching/matplotlib/matplotlib.html >`_.
This book has been written in restructured text format and generated using the rst2html.py command line available from the docutils python package.
If you want to rebuild the html output, from the top directory, type:
$ rst2html.py --link-stylesheet --cloak-email-addresses \ --toc-top-backlinks --stylesheet=book.css \ book.rst book.htmlThe sources are available from https://github.com/rougier/from-python-to-numpy .
This is not a Python beginner guide and you should have an intermediate level in Python and ideally a beginner level in numpy. If this is not the case, have a look at thefor a curated list of resources.
We will use usual naming conventions. If not stated explicitly, each script should import numpy, scipy and matplotlib as:
import numpy as np import scipy as sp import matplotlib.pyplot as pltWe'll use up-to-date versions (at the date of writing, i.e. January, 2017) of the different packages:
Packages Version Python 3.5.2 Numpy 1.11.2 Scipy 0.18.1 Matplotlib 1.5.3If you want to contribute to this book, you can:
Review chapters (please contact me) Report issues ( https://github.com/rougier/from-python-to-numpy/issues ) Suggest improvements ( https://github.com/rougier/from-python-to-numpy/pulls ) Correct English ( https://github.com/rougier/from-python-to-numpy/issues ) Design a better and more responsive html template for the book. Star the project ( https://github.com/rougier/from-python-to-numpy )If you're an editor interested in publishing this book, you can contact me if you agree to have this version and all subsequent versions open access (i.e. online atthis address), you know how to deal with restructured text (Word is not an option), you provide a real added-value as well as supporting services, and more importantly, you have a truly amazing latex book template (and be warned that I'm a bit picky about typography & design: Edward Tufte is my hero). Still here?
BookThis work is licensed under a Creative Commons Attribution-Non Commercial-Share Alike 4.0 International License . You are free to:
Share ― copy and redistribute the material in any medium or format Adapt ― remix, transform, and build upon the materialThe licensor cannot revoke these freedoms as long as you follow the license terms.
CodeThe code is licensed under the OSI-approved BSD 2-Clause License .
Numpy is all about vectorization. If you are familiar with Python, this is the main difficulty you'll face because you'll need to change your way of thinking and your new friends (among others) are named "vectors", "arrays", "views" or "ufuncs".
Let's take a very simple example, random walk.
One possible object oriented approach would be to define a RandomWalker class and to write with a walk method that would return current position after each (random) steps. It's nice, it's readable, but it is slow:
Object oriented approach class RandomWalker: def __init__(self): self.position = 0 def walk(self, n): self.position = 0 for i in range(n): yield self.position self.position += 2*random.randint(0, 1) - 1 walker = RandomWalker() walk = [position for position in walker.walk(1000)]Benchmarking gives us:
>>> from tools import timeit >>> walker = RandomWalker() >>> timeit("[position for position in walker.walk(n=10000)]", globals()) 10 loops, best of 3: 15.7 msec per loop Functional approachFor such a simple problem, we can probably save the class definition and concentrate only on the walk method that compute successive positions after each random steps.
def random_walk(n): position = 0 walk = [position] for i in range(n): position += 2*random.randint(0, 1)-1 walk.append(position) return walk walk = random_walk(1000)This new method saves some CPU cycles but not that much because this function is pretty much the same as in the object-oriented approach and the few cycles we saved probably come from the inner Python object-oriented machinery.
>>> from tools import timeit >>> timeit("random_walk(n=10000)]", globals()) 10 loops, best of 3: 15.6 msec per loop Vectorized approachBut we can do better using the itertools Python module that offers a set of functions creating iterators for efficient looping. If we observe that a random walk is an accumulation of steps, we can rewrite the function as:
def random_walk_faster(n=1000): from itertools import accumulate steps = random.sample([1, -1]*n, n) return list(accumulate(steps)) walk = random_walk_faster(1000)In fact, we've just vectorized our function. Instead of looping for picking sequential steps and add them to the current position, we fist generate all the steps at once and use the accumulate function to compute all the positions. We get rid of the loop and this makes things faster:
>>> from tools import timeit >>> timeit("random_walk_faster(n=10000)]", globals()) 10 loops, best of 3: 8.21 msec per loopWe gained 50% of computation-time compared to the previous version which is already good, but this new version makes numpy vectorization super simple, we just have to translate it into numpy methods.
def random_walk_fastest(n=1000): steps = 2*np.random.randint(0, 2, size=1000) - 1 return np.cumsum(steps) walk = random_walk_fastest(1000)Not too difficult, but we gained a factor 500x using numpy:
>>> from tools import timeit >>> timeit("random_walk_faster(n=10000)]", globals()) 1000 loops, best of 3: 14 usec per loopThis book is about vectorization, be it at the level of code or problem. We'll see this difference is important before looking at custom vectorization.
Before heading to the next chapter, I would like to warn you about a potential problem you may encounter once you'll have become familiar with numpy. It is a very powerful library and you can make wonders with it but, most of the time, this comes at the price of readability. If you don't comment your code at the time of writing, you'll be unable to guess what a function is doing after a few weeks (or even days). For example, can you tell what the two functions below are doing? Probably you can tell for the first one, but unlikely for the second (or you don't need to read this book).
def function_1(seq, sub): return [i for i in range(len(seq) - len(sub)) if seq[i:i+len(sub)] == sub] def function_2(seq, sub): target = np.dot(sub, sub) candidates = np.where(np.correlate(seq, sub, mode='valid') == target)[0] check = candidates[:, np.newaxis] + np.arange(len(sub)) mask = np.all((np.take(seq, check) == sub), axis=-1) return candidates[mask]As you may have guessed, the second function is the vectorized-optimized-faster-numpy version of the first function. It is 10 times faster than the pure Python version, but it is hardly readable.
Contents Uniform vectorization Temporal vectorization Spatial vectorization
Code vectorization means that the problem you're trying to solve is inherently vectorizable and only requires a few numpy tricks to make it faster. Of course it does not mean it is easy nor straightforward, but at least it does not necessitate to totally rethink your problem (as it will be the case in theProblem vectorizationchapter). Still, it may require some experience to see where code can be vectorized. Let's illustrate this through the most simple example where we want to sum up two lists of integers. One simple way using pure Python is:
def add_python(Z1,Z2): return [z1+z2 for (z1,z2) in zip(Z1,Z2)]This first naive solution can be vectorized very easily using numpy:
def add_numpy(Z1,Z2): return np.add(Z1,Z2)Without any surprise, benchmarking the two approaches shows the second method is the fastest with one order of magnitude.
>>> Z1 = random.sample(range(1000), 100) >>> Z2 = random.sample(range(1000), 100) >>> timeit("add_python(Z1, Z2)", globals()) 1000 loops, best of 3: 68 usec per loop >>> timeit("add_numpy(Z1, Z2)", globals()) 10000 loops, best of 3: 1.14 usec per loopNot only is the second approach faster, but it also naturally adapts to the shape of Z1 and Z2 . This is the reason why we did not write Z1 + Z2 because it would not work if Z1 and Z2 were both lists. In the first Python method, the inner + is interpreted differently depending on the nature of the two objects such that if we consider two nested lists, we get the following outputs:
>>> Z1 = [[1, 2], [3, 4]] >>> Z2 = [[5, 6], [7, 8]] >>> Z1 + Z2 [[1, 2], [3, 4], [5, 6], [7, 8]] >>> add_python(Z1, Z2) [[1, 2, 5, 6], [3, 4, 7, 8]] >>> add_numpy(Z1, Z2) [[ 6 8] [10 12]]The first method concatenates the two lists together, the second method concatenates the internal lists together and the last one computes what is (numerically) expected. As an exercise, you can rewrite the python version such that it accepts nested lists of any depth.
Uniform vectorizationUniform vectorization is the simplest form of vectorization where all the elements share the same computation at every time step with no specific processing for any element. One stereotypical case is the Game of Life that has been invented by John Conway (see below) and is one of the earliest examples of cellular automata. Those cellular automata can be conveniently considered as an array of cells that are connected together with the notion of neighbours and their vectorization is straightforward. Let me first define the game and we'll see how to vectorize it.
Figure 4.1Conus textile snail exhibits a cellular automaton pattern on its shell. Image by Richard Ling , 2005.

Note
Excerpt from the Wikipedia entry on the Game of Life
The Game of Life is a cellular automaton devised by the British mathematician John Horton Conway in 1970. It is the best-known example of a cellular automaton. The "game" is actually a zero-player game, meaning that its evolution is determined by its initial state, needing no input from human players. One interacts with the Game of Life by creating an initial configuration and observing how it evolves.
The universe of the Game of Life is an infinite two-dimensional orthogonal grid of square cells, each of which is in one of two possible states, live or dead. Every cell interacts with its eight neighbours, which are the cells that are directly horizontally, vertically, or diagonally adjacent. At each step in time, the following transitions occur:
Any live cell with fewer than two live neighbours dies, as if by needs caused by under population. Any live cell with more than three live neighbours dies, as if by overcrowding. Any live cell with two or three live neighbours lives, unchanged, to the next generation. Any dead cell with exactly three live neighbours becomes a live cell.The initial pattern constitutes the 'seed' of the system. The first generation is created by applying the above rules simultaneously to every cell in the seed births and deaths happen simultaneously, and the discrete moment at which this happens is sometimes called a tick. (In other words, each generation is a pure function of the one before.) The rules continue to be applied repeatedly to create further generations.
Python implementationNote
We could have used the more efficient python array interface but it is more convenient to use the familiar list object.
In pure Python, we can code the Game of Life using a list of lists representing the board where cells are supposed to evolve. Such a board will be equipped with border of 0 that allows to accelerate things a bit by avoiding to have specific tests for borders when counting the number of neighbours.
Z = [[0,0,0,0,0,0], [0,0,0,1,0,0], [0,1,0,1,0,0], [0,0,1,1,0,0], [0,0,0,0,0,0], [0,0,0,0,0,0]]Taking the border into account, counting neighbours is then straightforward:
def compute_neighbours(Z): shape = len(Z), len(Z[0]) N = [[0,]*(shape[0]) for i in range(shape[1])] for x in range(1,shape[0]-1): for y in range(1,shape[1]-1): N[x][y] = Z[x-1][y-1]+Z[x][y-1]+Z[x+1][y-1] \ + Z[x-1][y] +Z[x+1][y] \ + Z[x-1][y+1]+Z[x][y+1]+Z[x+1][y+1] return NTo iterate one step in time, we then simply count the number of neighbours for each internal cell and we update the whole board according to the 4 aforementioned rules:
def iterate(Z): N = compute_neighbours(Z) for x in range(1,shape[0]-1): for y in range(1,shape[1]-1): if Z[x][y] == 1 and (N[x][y] < 2 or N[x][y] > 3): Z[x][y] = 0 elif Z[x][y] == 0 and N[x][y] == 3: Z[x][y] = 1 return ZThe figure below shows 4 iterations on a 4x4 area where the initial state is a glider , a structure discovered by Richard K. Guy in 1970.
Figure 4.2The glider pattern is known to replicate itself one step diagonally in 4 iterations.

Starting from the Python version, the vectorization of the Game of Life requires two parts, one responsible for counting the neighbours and one responsible for enforcing the rules. Neighbour-counting is relatively easy if we remember we took care of adding a null border around the arena. By considering partial views of the arena we can actually access neighbours quite intuitively as illustrated below for the one-dimensional case:
┏━━━┳━━━┳━━━┓───┬───┐ Z[:-2] ┃ 0 ┃ 1 ┃ 1 ┃ 1 │ 0 │ (left neighbours) ┗━━━┻━━━┻━━━┛───┴───┘ ↓ ┌───┏━━━┳━━━┳━━━┓───┐ Z[1:-1] │ 0 ┃ 1 ┃ 1 ┃ 1 ┃ 0 │ (actual cells) └───┗━━━┻━━━┻━━━┛───┘ ↑ ┌───┬───┏━━━┳━━━┳━━━┓ Z[+2:] │ 0 │ 1 ┃ 1 ┃ 1 ┃ 0 ┃ (right neighbours) └───┴───┗━━━┻━━━┻━━━┛Going to the two dimensional case requires just a bit of arithmetic to make sure to consider all the eight neighbours.
N = np.zeros(Z.shape, dtype=int) N[1:-1,1:-1] += (Z[ :-2, :-2] + Z[ :-2,1:-1] + Z[ :-2,2:] + Z[1:-1, :-2] + Z[1:-1,2:] + Z[2: , :-2] + Z[2: ,1:-1] + Z[2: ,2:])For the rule enforcement, we can write a first version using the argwhere method that will give us the indices where a given condition is True.
# Flatten arrays N_ = N.ravel() Z_ = Z.ravel() # Apply rules R1 = np.argwhere( (Z_==1) & (N_ < 2) ) R2 = np.argwhere( (Z_==1) & (N_ > 3) ) R3 = np.argwhere( (Z_==1) & ((N_==2) | (N_==3)) ) R4 = np.argwhere( (Z_==0) & (N_==3) ) # Set new values Z_[R1] = 0 Z_[R2] = 0 Z_[R3] = Z_[R3] Z_[R4] = 1 # Make sure borders stay null Z[0,:] = Z[-1,:] = Z[:,0] = Z[:,-1] = 0Even if this first version does not use nested loops, it is far from optimal because of the use of the 4 argwhere calls that may be quite slow. We can instead factorize the rules into cells that will survive (stay at 1) and cells that will give birth. For doing this, we can take advantage of Numpy boolean capability and write quite naturally:
Note
We did no write Z = 0 as this would simply assign the value 0 to Z that would then become a simple scalar.
birth = (N==3) & (Z[1:-1,1:-1]==0) survive = ((N==2) | (N==3)) & (Z[1:-1,1:-1]==1) Z[...] = 0 Z[1:-1,1:-1][birth | survive] = 1If you look at the birth and survive lines, you'll see that these two variables are arrays that can be used to set Z values to 1 after having cleared it.
Figure 4.3The Game of Life. Gray levels indicate how much a cell has been active in the past.
Your browser does not support the video tag. Reaction and diffusion of chemical species can produce a variety of patterns, reminiscent of those often seen in nature. The Gray-Scott equations model such a reaction. For more information on this chemical system see the article Complex Patterns in a Simple System (John E. Pearson, Science, Volume 261, 1993). Let's consider two chemical species U and V with respective concentrations u