In [1]:
# - compatibility with Python 3
from __future__ import print_function  # print('me') instead of print 'me'
from __future__ import division  # 1/2 == 0.5, not 0
In [2]:
# - show figures inside the notebook
%matplotlib inline
In [3]:
# - import common modules
import numpy as np  # the Python array package
import matplotlib.pyplot as plt  # the Python plotting package

Repeating data over axes with np.tile

np.tile can be used to replicate data within an array.

For example, let's say I have a row vector R:

In [4]:
R = np.array([[1, 2]])
R
Out[4]:
array([[1, 2]])

I might want to make a new array, where I repeat the row 4 times, to make a 4 by 2 array. I specify the number of repeats on each axis. In this case I want to repeat the first axis 4 times, and the second axis I don't want to repeat - so I repeat 1 time:

In [5]:
np.tile(R, [4, 1])
Out[5]:
array([[1, 2],
       [1, 2],
       [1, 2],
       [1, 2]])

In fact, we don't often use np.tile because we can usually get the same effect with broadcasting.

Basic broadcasting

Numpy has a very useful but somewhat advanced feature called broadcasting. Broadcasting is a system that numpy uses when trying to match the shapes of two arrays, that do not have the same shape.

Let's say you have two arrays, A and B:

In [6]:
A = np.array([[2, 3], [4, 5], [6, 7]])
A
Out[6]:
array([[2, 3],
       [4, 5],
       [6, 7]])
In [7]:
B = np.array([1, 3])
B
Out[7]:
array([1, 3])

A has shape (3, 2) and B has shape (2,). What will happen if I try and add the arrays A and B?

One thing that could happen, is that numpy raises an error. Another thing that might happen, is that numpy might realize that you meant to match the only axis of B to the last axis of A, and make enough copies of the only axis of B to match the 3 columns of A. Something like this:

In [8]:
new_B = np.tile(B, (3, 1))
new_B
Out[8]:
array([[1, 3],
       [1, 3],
       [1, 3]])

Then the subtraction would be:

In [9]:
A - new_B
Out[9]:
array([[1, 0],
       [3, 2],
       [5, 4]])

In fact, that is exactly what numpy does by default, using broadcasting:

In [10]:
A - B
Out[10]:
array([[1, 0],
       [3, 2],
       [5, 4]])

A simplified version of the rules that numpy uses is the following:

  • If B has fewer dimensions than A, prepend as many new 1-length dimensions to B as necessary (shape of B goes from (2,) to (1, 2) in this case;
  • If there are any 1-length dimensions in the new B shape, and the corresponding dimension in A is not 1, make as many repeats of the data along this axis as needed to fill out the 1-length dimension to the length of the matching dimension in A.
  • Do the operation on the original A and the modified B.

So, in our case, that would look like:

In [11]:
# Prepend as many 1-length dimensions as necessary 
# to give B same number of dimensions as A
B_1 = B.reshape(1, 2)
B_1
Out[11]:
array([[1, 3]])
In [12]:
# For each 1-length dimension of B, where matching dimension of A has >1 length:
# Replicate data to extend 1-length dimension to same length as A dimension
B_2 = np.tile(B_1, [3, 1])
B_2
Out[12]:
array([[1, 3],
       [1, 3],
       [1, 3]])
In [13]:
# Do the operation on A and new B
A - B_2
Out[13]:
array([[1, 0],
       [3, 2],
       [5, 4]])

Dictionaries - key, value containers

The dictionary is a very useful fundamental type in Python.

The dictionary is made up of key, value pairs.

The key is the thing that allows you to look up a value. Like this:

In [14]:
some_scores = {'matthew': 11, 'jb': 21}
some_scores
Out[14]:
{'jb': 21, 'matthew': 11}

Here jb and matthew are keys. If I want the jb value, I can look it up, using the jb key:

In [15]:
some_scores['jb']
Out[15]:
21

Same for the matthew value:

In [16]:
some_scores['matthew']
Out[16]:
11

I can add a key, value pair, by assigning like this:

In [17]:
some_scores['stefan'] = 2  # oh dear
In [18]:
some_scores
Out[18]:
{'jb': 21, 'matthew': 11, 'stefan': 2}

The keys are often strings, as here, but the keys can be lots of different things, such as numbers or tuples:

In [19]:
some_scores[99] = 7  # 0 is just a number, doesn't refer to position
some_scores
Out[19]:
{99: 7, 'jb': 21, 'matthew': 11, 'stefan': 2}

As you may see from the output above, the order of the keys is unpredictable, because Python arranges them as it likes. Only the relationship between the keys and the values is guaranteed.

We'll see dictionaries coming up often as we use Python more.

Setting defaults for plotting

We often want to display images with a gray colormap, and with nearest neighbor interpolation:

In [20]:
import nibabel as nib
img = nib.load('ds114_sub009_t2r1.nii')
data = img.get_data()
# Not like this:
plt.imshow(data[:, :, 15, 0])
Out[20]:
<matplotlib.image.AxesImage at 0x10a212f90>
In [21]:
# But like this:
plt.imshow(data[:, :, 15, 0], cmap='gray', interpolation='nearest')
Out[21]:
<matplotlib.image.AxesImage at 0x10a379110>

Notice this second image is - grayscale - but also - less blurry - showing us the individual pixels without interpolating between them.

We can tell matplotlib to use these settings by default, by using the rcParams dictionary:

In [22]:
# Set gray colormap and nearest neighbor interpolation by default
plt.rcParams['image.cmap'] = 'gray'
plt.rcParams['image.interpolation'] = 'nearest'

Now we don't need to set these setting specifically:

In [23]:
plt.imshow(data[:, :, 15, 0])
Out[23]:
<matplotlib.image.AxesImage at 0x10a6ccd90>

Arrays are mutable

In [24]:
arr = np.arange(10)
arr
Out[24]:
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

We can make another variable point to this, but that's probably not a good idea:

In [25]:
arr2 = arr
arr2[0] = 42
arr
Out[25]:
array([42,  1,  2,  3,  4,  5,  6,  7,  8,  9])

Oops, we have modified the original array.

Copying arrays

To avoid stepping on the toes of another array during processing, it is often a good idea to make an explicit copy:

In [26]:
arr3 = arr.copy()  # The copy method

We can modify the new copy without modifying the original:

In [27]:
arr3[0] = 99
arr3
Out[27]:
array([99,  1,  2,  3,  4,  5,  6,  7,  8,  9])
In [28]:
arr
Out[28]:
array([42,  1,  2,  3,  4,  5,  6,  7,  8,  9])