Welcome to the jupyter notebook! To run any cell, press Shit+Enter or Ctrl+Enter.

IMPORTANT : Please have a look at Help->User Interface Tour and Help->Keyboard Shortcuts in the toolbar above that will help you get started.


In [1]:
# Useful starting lines
%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
%load_ext autoreload
%autoreload 2

Notebook Basics

A cell contains any type of python inputs (expression, function definitions, etc...). Running a cell is equivalent to input this block in the python interpreter. The notebook will print the output of the last executed line.


In [2]:
1


Out[2]:
1

In [3]:
x = [2,3,4]

def my_function(l):
    l.append(12)

In [4]:
my_function(x)

x


Out[4]:
[2, 3, 4, 12]

In [5]:
# Matplotlib is used for plotting, plots are directly embedded in the
# notebook thanks to the '%matplolib inline' command at the beginning
plt.hist(np.random.randn(10000), bins=40)
plt.xlabel('X label')
plt.ylabel('Y label')


Out[5]:
<matplotlib.text.Text at 0x10d42a320>

Numpy Basics

IMPORTANT : the numpy documentation is quite good. The Notebook system is really good to help you. Use the Auto-Completion with Tab, and use Shift+Tab to get the complete documentation about the current function (when the cursor is between the parenthesis of the function for instance).

For example, you want to multiply two arrays. np.mul + Tab complete to the only valid function np.multiply. Then using Shift+Tab you learn np.multiply is actually the element-wise multiplication and is equivalent to the * operator.


In [6]:
np.multiply


Out[6]:
<ufunc 'multiply'>

Creation of arrays

Creating ndarrays (np.zeros, np.ones) is done by giving the shape as an iterable (List or Tuple). An integer is also accepted for one-dimensional array.

np.eye creates an identity matrix.

You can also create an array by giving iterables to it.

(NB : The random functions np.random.rand and np.random.randn are exceptions though)


In [7]:
np.zeros(4)


Out[7]:
array([ 0.,  0.,  0.,  0.])

In [8]:
np.eye(3)


Out[8]:
array([[ 1.,  0.,  0.],
       [ 0.,  1.,  0.],
       [ 0.,  0.,  1.]])

In [9]:
np.array([[1,3,4],[2,5,6]])


Out[9]:
array([[1, 3, 4],
       [2, 5, 6]])

In [79]:
np.arange(10)  # NB : np.array(range(10)) is a slightly more complicated equivalent


Out[79]:
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

In [80]:
np.random.randn(3, 4) # normal distributed values


Out[80]:
array([[ 2.2868779 , -0.76795226,  1.71747877, -0.90427916],
       [-2.09637536, -0.06840064,  0.51132137,  0.0345745 ],
       [-0.04919065, -0.90229518, -1.10826166, -1.18147872]])

In [81]:
# 3-D tensor
tensor_3 = np.ones((2, 4, 2))
tensor_3


Out[81]:
array([[[ 1.,  1.],
        [ 1.,  1.],
        [ 1.,  1.],
        [ 1.,  1.]],

       [[ 1.,  1.],
        [ 1.,  1.],
        [ 1.,  1.],
        [ 1.,  1.]]])

ndarray basics

A ndarray python object is just a reference to the data location and its characteristics.

All numpy operations applying on an array can be called np.function(a) or a.function() (i.e np.sum(a) or a.sum())

It has an attribute shape that returns a tuple of the different dimensions of the ndarray. It also has an attribute dtype that describes the type of data of the object (default type is float64)

WARNING because of the object structure, unless you call copy() copying the reference is not copying the data.


In [13]:
tensor_3.shape, tensor_3.dtype


Out[13]:
((2, 4, 2), dtype('float64'))

In [14]:
a = np.array([[1.0, 2.0], [5.0, 4.0]])
b = np.array([[4, 3], [2, 1]])
(b.dtype, a.dtype) # each array has a data type (casting rules apply for int -> float)


Out[14]:
(dtype('int64'), dtype('float64'))

In [15]:
np.array(["Mickey", "Mouse"]) # can hold more than just numbers


Out[15]:
array(['Mickey', 'Mouse'], 
      dtype='<U6')

In [16]:
a = np.array([[1.0, 2.0], [5.0, 4.0]])
b = a  # Copying the reference only
b[0,0] = 3
a


Out[16]:
array([[ 3.,  2.],
       [ 5.,  4.]])

In [17]:
a = np.array([[1.0, 2.0], [5.0, 4.0]])
b = a.copy()  # Deep-copy of the data
b[0,0] = 3
a


Out[17]:
array([[ 1.,  2.],
       [ 5.,  4.]])

Basic operators are working element-wise (+, -, *, /)

When trying to apply operators for arrays with different sizes, they are very specific rules that you might want to understand in the future : http://docs.scipy.org/doc/numpy/user/basics.broadcasting.html


In [18]:
np.ones((2, 4)) * np.random.randn(2, 4)


Out[18]:
array([[ 0.58589148, -1.47037601, -1.59253615,  0.94146717],
       [ 1.52256705,  1.2312153 , -1.48255074,  0.23582932]])

In [19]:
np.eye(3) - np.ones((3,3))


Out[19]:
array([[ 0., -1., -1.],
       [-1.,  0., -1.],
       [-1., -1.,  0.]])

In [20]:
print(a)
print(a.shape)  # Get shape
print(a.shape[0])  # Get size of first dimension


[[ 1.  2.]
 [ 5.  4.]]
(2, 2)
2

Accessing elements and slicing

For people uncomfortable with the slicing of arrays, please have a look at the 'Indexing and Slicing' section of http://www.python-course.eu/numpy.php


In [21]:
print(a[0])  # Get first line (slice for the first dimension)
print(a[:, 1])  # Get second column (slice for the second dimension)
print(a[0, 1])  # Get first line second column element


[ 1.  2.]
[ 2.  4.]
2.0

Changing the shape of arrays

ravel creates a flattened view of an array (1-D representation) whereas flatten creates flattened copy of the array.

reshape allows in-place modification of the shape of the data. transpose shuffles the dimensions.

np.newaxis allows the creation of empty dimensions.


In [22]:
a = np.array([[1.0, 2.0], [5.0, 4.0]])
b = np.array([[4, 3], [2, 1]])
v = np.array([0.5, 2.0])

In [23]:
print(a)
print(a.T)  # Equivalent : a.tranpose(), np.transpose(a)
print(a.ravel())


[[ 1.  2.]
 [ 5.  4.]]
[[ 1.  5.]
 [ 2.  4.]]
[ 1.  2.  5.  4.]

In [24]:
c = np.random.randn(4,5)
print(c.shape)
print(c[np.newaxis].shape)  # Adding a dimension
print(c.T.shape)  
print(c.reshape([10,2]).shape)
print(c)
print(c.reshape([10,2]))


(4, 5)
(1, 4, 5)
(5, 4)
(10, 2)
[[ 0.86839939  0.8715639   0.45321045  0.07676759  1.847156  ]
 [ 0.05133845 -0.47399277  1.97738607 -0.6663665   0.43934335]
 [ 1.05028791 -1.90099229  0.00852622  0.52136128 -0.73067448]
 [-1.0585324  -0.42458638  0.40902682 -1.15218458  0.20909564]]
[[ 0.86839939  0.8715639 ]
 [ 0.45321045  0.07676759]
 [ 1.847156    0.05133845]
 [-0.47399277  1.97738607]
 [-0.6663665   0.43934335]
 [ 1.05028791 -1.90099229]
 [ 0.00852622  0.52136128]
 [-0.73067448 -1.0585324 ]
 [-0.42458638  0.40902682]
 [-1.15218458  0.20909564]]

In [25]:
a.reshape((-1, 1)) # a[-1] means 'whatever needs to go there'


Out[25]:
array([[ 1.],
       [ 2.],
       [ 5.],
       [ 4.]])

Reduction operations

Reduction operations (np.sum, np.max, np.min, np.std) work on the flattened ndarray by default. You can specify the reduction axis as an argument


In [26]:
np.sum(a), np.sum(a, axis=0), np.sum(a, axis=1) # reduce-operations reduce the whole array if no axis is specified


Out[26]:
(12.0, array([ 6.,  6.]), array([ 3.,  9.]))

Linear-algebra operations


In [27]:
np.dot(a, b) # matrix multiplication


Out[27]:
array([[  8.,   5.],
       [ 28.,  19.]])

In [28]:
# Other ways of writing matrix multiplication, the '@' operator for matrix multiplication
# was introduced in Python 3.5
np.allclose(a.dot(b), a @ b)


Out[28]:
True

In [29]:
# For other linear algebra operations, use the np.linalg module
np.linalg.eig(a)  # Eigen-decomposition


Out[29]:
(array([-1.,  6.]), array([[-0.70710678, -0.37139068],
        [ 0.70710678, -0.92847669]]))

In [30]:
print(np.linalg.inv(a))  # Inverse
np.allclose(np.linalg.inv(a) @ a, np.identity(a.shape[1]))  # a^-1 * a = Id


[[-0.66666667  0.33333333]
 [ 0.83333333 -0.16666667]]
Out[30]:
True

In [31]:
np.linalg.solve(a, v) # solves ax = v


Out[31]:
array([ 0.33333333,  0.08333333])

Grouping operations

Grouping operations (np.stack, np.hstack, np.vstack, np.concatenate) take an iterable of ndarrays and not ndarrays as separate arguments : np.concatenate([a,b]) and not np.concatenate(a,b).


In [32]:
np.hstack([a, b])


Out[32]:
array([[ 1.,  2.,  4.,  3.],
       [ 5.,  4.,  2.,  1.]])

In [33]:
np.vstack([a, b])


Out[33]:
array([[ 1.,  2.],
       [ 5.,  4.],
       [ 4.,  3.],
       [ 2.,  1.]])

In [34]:
np.vstack([a, b]) + v # broadcasting


Out[34]:
array([[ 1.5,  4. ],
       [ 5.5,  6. ],
       [ 4.5,  5. ],
       [ 2.5,  3. ]])

In [35]:
np.hstack([a, b]) + v # does not work


---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-35-ee6037013b2b> in <module>()
----> 1 np.hstack([a, b]) + v # does not work

ValueError: operands could not be broadcast together with shapes (2,4) (2,) 

In [36]:
np.hstack([a, b]) + v.T # transposing a 1-D array achieves nothing


---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-36-d62836ec8967> in <module>()
----> 1 np.hstack([a, b]) + v.T # transposing a 1-D array achieves nothing

ValueError: operands could not be broadcast together with shapes (2,4) (2,) 

In [37]:
np.hstack([a, b]) + v.reshape((-1, 1)) # reshaping to convert v from a (2,) vector to a (2,1) matrix


Out[37]:
array([[ 1.5,  2.5,  4.5,  3.5],
       [ 7. ,  6. ,  4. ,  3. ]])

In [38]:
np.hstack([a, b]) + v[:, np.newaxis] # equivalently, we can add an axis


Out[38]:
array([[ 1.5,  2.5,  4.5,  3.5],
       [ 7. ,  6. ,  4. ,  3. ]])

Working on subset of the elements

We have two ways in order to apply operations on subparts of arrays (besides slicing).

Slicing reminders


In [39]:
r = np.random.random_integers(0, 9, size=(3, 4))

In [40]:
r


Out[40]:
array([[6, 6, 4, 5],
       [6, 4, 8, 2],
       [7, 0, 4, 9]])

In [41]:
r[0], r[1]


Out[41]:
(array([6, 6, 4, 5]), array([6, 4, 8, 2]))

In [42]:
r[0:2]


Out[42]:
array([[6, 6, 4, 5],
       [6, 4, 8, 2]])

In [43]:
r[1][2] # regular python


Out[43]:
8

In [44]:
r[1, 2] # numpy


Out[44]:
8

In [45]:
r[:, 1:3]


Out[45]:
array([[6, 4],
       [4, 8],
       [0, 4]])

Binary masks

Using logical operations on arrays give a binary mask. Using a binary mask as indexing acts as a filter and outputs just the very elements where the value is True. This gives a memoryview of the array that can get modified.


In [46]:
r > 5  # Binary element-wise result


Out[46]:
array([[ True,  True, False, False],
       [ True, False,  True, False],
       [ True, False, False,  True]], dtype=bool)

In [47]:
r[r > 5]  # Use the binary mask as filter


Out[47]:
array([6, 6, 6, 8, 7, 9])

In [48]:
r[r > 5] = 999  # Modify the corresponding values with a constant

In [49]:
r


Out[49]:
array([[999, 999,   4,   5],
       [999,   4, 999,   2],
       [999,   0,   4, 999]])

Working with indices

The second way to work on subpart of arrays are through indices. Usually you'd use one array per dimension with matching indices.

WARNING : indices are usually slower than binary masks because it is harder to be parallelized by the underlying BLAS library.


In [52]:
# Get the indices where the condition is true, gives a tuple whose length
# is the number of dimensions of the input array
np.where(r == 999)


Out[52]:
(array([0, 0, 1, 1, 2, 2]), array([0, 1, 0, 2, 0, 3]))

In [57]:
print(np.where(np.arange(10) < 5))  # Is a 1-tuple
np.where(np.arange(10) < 5)[0]  # Accessing the first element gives the indices array


(array([0, 1, 2, 3, 4]),)
Out[57]:
array([0, 1, 2, 3, 4])

In [59]:
np.where(r == 999, -10, r+1000)  # Ternary condition, if True take element from first array, otherwise from second


Out[59]:
array([[ -10,  -10, 1004, 1005],
       [ -10, 1004,  -10, 1002],
       [ -10, 1000, 1004,  -10]])

In [62]:
r[(np.array([1,2]), np.array([2,2]))]  # Gets the view corresponding to the indices. NB : iterable of arrays as indexing


Out[62]:
array([999,   4])

Working with arrays, examples

Thanks to all these tools, you should be able to avoid writing almost any for-loops which are extremely costly in Python (even more than in Matlab, because good JIT engines are yet to come). In case you really need for-loops for array computation (usually not needed but it happens) have a look at http://numba.pydata.org/ (For advanced users)

Counting the number of positive elements that satisfy a condition


In [69]:
numbers = np.random.randn(1000, 1000)

In [70]:
%%timeit  # Naive version
my_sum = 0
for n in numbers.ravel():
    if n>0:
        my_sum += n


1 loops, best of 3: 262 ms per loop

In [71]:
%timeit np.sum(numbers > 0)


1000 loops, best of 3: 1.68 ms per loop

Compute polynomial for a lot of values


In [76]:
X = np.random.randn(10000)

In [77]:
%%timeit  # Naive version
my_result = np.zeros(len(X))
for i, x in enumerate(X.ravel()):
    my_result[i] = 1 + x + x**2 + x**3 + x**4


100 loops, best of 3: 12.2 ms per loop

In [78]:
%timeit 1 + X + X**2 + X**3 + X**4


1000 loops, best of 3: 759 µs per loop

Scipy

Scipy is a collection of libraries more specialized than Numpy. It is the equivalent of toolboxes in Matlab.

Have a look at their collection : http://docs.scipy.org/doc/scipy-0.18.0/reference/

Many traditionnal functions are coded there.


In [82]:
X = np.random.randn(1000)

In [85]:
from scipy.fftpack import fft
plt.plot(fft(X).real)


Out[85]:
[<matplotlib.lines.Line2D at 0x11076f320>]

In [ ]: