Vectorization Part 1 (Basic examples)

Looping in Python is very slow compared with vectorized operations in numpy. So, we will usually want to do as much work with vectorized operations as possible.

We will first review the two most cmmmonly used regular looping constructs in Python to find the first 10 square numbers, i.e. the for loop and the list comprehesnion.


In [1]:
%pylab


Using matplotlib backend: MacOSX
Populating the interactive namespace from numpy and matplotlib

In [2]:
# using a for loop and appending to an empty vector
squares1 = []
for x in range(1, 11):
    squares1.append(x*x)
print "squares1  = ", squares1

# using a list comprehsnsion. 
# List comprehensions are modeled after the set builder notation in mathematics 
# and a similar construct in the funcitional language Haskell. For our puproses,
# you can just think of them as a shorthand way of generating lists.
squares3 = [x*x for x in range(1, 11)]
print "squares3  = ", squares3

# I will be using list comprehensions instead of for loops in the examples below 
# since there is less to type. Here are more examples of list comprehensions to show how easy they are.
print
print "More list comprehension examples."
print [i for i in range(10)]
print [ch for ch in 'hello world']
print [word for word in 'hello world'.split()]


squares1  =  [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
squares3  =  [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

More list comprehension examples.
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
['h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd']
['hello', 'world']

In [3]:
# sometimes when constructing our list, we only want to include items if they meet some condition.
# Building on previous example, suppose we only wanted square numbers that were also even. A necessary and
# suffiicient condition for this if if the original (unsquared) number is also even.

# using a for loop
esquares1 = []
for x in range(1, 11):
    if x%2 == 0:
        esquares1.append(x*x)
print "even squares1  = ", esquares1

# using a list comprehsnsion. 
# List comprehensions can include a condition by tagging on an "if" condition at the end.
esquares3 = [x*x for x in range(1, 11) if x%2 == 0]
print "even squares3  = ", esquares3

print
print "More list comprehension examples with filtering"
print [i for i in range(10) if i*i > 10] 
print [ch for ch in 'hello world' if ch not in 'aieou']
print [word for word in 'hello world'.split() if word.startswith('hell')]


even squares1  =  [4, 16, 36, 64, 100]
even squares3  =  [4, 16, 36, 64, 100]

More list comprehension examples with filtering
[4, 5, 6, 7, 8, 9]
['h', 'l', 'l', ' ', 'w', 'r', 'l', 'd']
['hello']

Basic examples

Now we can see some vectorized examples.


In [4]:
import numpy as np

# first let's repeat the above examples in numpy with vectorization.
xs = np.arange(1,11)
squares = xs**2
print "squares in numpy = ", squares

# we can compare the time taken.
%timeit [x*x for x in xs]
%timeit xs**2

# the speed difference becomes more obvioius as the size of the vecotr increases
print
print "Squaring a vector with 1000 entries"
xs = np.arange(1, 1001)
%timeit [x*x for x in xs]
%timeit xs**2


squares in numpy =  [  1   4   9  16  25  36  49  64  81 100]
100000 loops, best of 3: 6.63 µs per loop
100000 loops, best of 3: 1.87 µs per loop

Squaring a vector with 1000 entries
1000 loops, best of 3: 508 µs per loop
100000 loops, best of 3: 2.68 µs per loop

In [5]:
# Some more simple examples comparing list comprehensions and vectorized operations.

# add two vectors
n = 1000
a = np.random.random(n)
b = np.random.random(n)

# slow looping
%timeit [u+v for (u, v) in zip(a, b)]

# fast vectorized operation
%timeit a + b


1000 loops, best of 3: 770 µs per loop
100000 loops, best of 3: 2.7 µs per loop

Vectorized operations are often usefully combined with logical indexing. Unlike regular indexing, logical indexing uses a vector of Boolean values to extract items from a vector or matrix. The Boolean vector is of the same shape as the original vector, and logical indexing returens the items where the corresponding Boolean entry is True.


In [6]:
# logical indexing to select even numbers
xs = np.arange(10)
idx = np.array([True, False, True, False, True, False, True, False, True, False])
print "Orignal vector =", xs
print "Logically indexed vecotr =", xs[idx]


Orignal vector = [0 1 2 3 4 5 6 7 8 9]
Logically indexed vecotr = [0 2 4 6 8]

In [7]:
# Typically, comparison operators (e..g >, <=, ==) ae used to generate the Boolean indices

print xs%2 == 0 # index of even numbers
print xs[xs%2 == 0] # using logical indexing to select even numbers


[ True False  True False  True False  True False  True False]
[0 2 4 6 8]

In [8]:
# Logical indexing is itself a vectorized operation

# slow looping
%timeit [x for x in xs if x%2 == 0]

# fast vectorized operation
%timeit xs[xs%2 == 0]


10000 loops, best of 3: 20.6 µs per loop
100000 loops, best of 3: 7.76 µs per loop

As an example of how logical indexing can eliminate the need for looping, we can make mulitple decisions based on the outcome of a random variable using logical indexing. This comes in useful in many stochastic simulations. Suppose you have a stocahstic simulation where 99% of the time, event A occurs but 1% of the time, event B occurs instead. Logical indexing on a random vector is much faster than looping.


In [9]:
# making multiple decisions based on outcome of a random variable
n = 1000
threshold = 0.01

# slow loop
%timeit
rs = []
for r in np.arange(n):
    if np.random.rand() < threshold:
       rs.append(r)
        
        
%timeit [r for r in np.arange(n) if np.random.rand() < threshold]

# fast vector operation
%timeit np.arange(n)[np.random.rand(n) < threshold]


1000 loops, best of 3: 381 µs per loop
10000 loops, best of 3: 23.9 µs per loop

In [10]:
# Using the appropriate numpy operation 
# instead of the equivalent Python idiom is usually faster for large data sets.

print "Finding unique elements in a small data set"
n =100
xs = np.random.randint(0, 100, n) # retunrs numpy array of n random integers in the range 0,1,2,...,99
print "numpy version       ",
%timeit np.unique(xs)
print "Python idiom version",
%timeit list(set(xs))


Finding unique elements in a small data set
numpy version        10000 loops, best of 3: 19.7 µs per loop
Python idiom version 10000 loops, best of 3: 22.5 µs per loop

In [11]:
print "\nFinding unique elements in a large data set"
n =10000000
xs = np.random.randint(0, 100, n)
print "numpy version       ",
%timeit np.unique(xs)
print "Python idiom version",
%timeit list(set(xs))


Finding unique elements in a large data set
numpy version        1 loops, best of 3: 447 ms per loop
Python idiom version 1 loops, best of 3: 1.46 s per loop