Some revision and some hints for the exercises

In [1]:
# Compatibility with Python 3, as always
from __future__ import print_function  # print('me') instead of print 'me'
from __future__ import division  # 1/2 == 0.5, not 0

Tuples

Tuples are lists that you cannot modify.

Put another way, tuples are immutable lists.

In [2]:
my_list = ['list values', False, 10, 10.1]  # Lists use square brackets
my_tuple = ('some values', True, 1, 0.1)  # Tuples use parentheses
In [3]:
my_list[0] = 'different value'
my_list
Out[3]:
['different value', False, 10, 10.1]
In [4]:
my_tuple[0] = 'another value'
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-4-34e0ade966e4> in <module>()
----> 1 my_tuple[0] = 'another value'

TypeError: 'tuple' object does not support item assignment
In [5]:
my_list.append('value at end')
my_list
Out[5]:
['different value', False, 10, 10.1, 'value at end']
In [6]:
my_tuple.append('something')
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-6-10a45a17ed57> in <module>()
----> 1 my_tuple.append('something')

AttributeError: 'tuple' object has no attribute 'append'

A little detail: a literal value for a tuple with a single value.

You might think this:

In [7]:
small_tuple = (1)
small_tuple
Out[7]:
1
In [8]:
type(small_tuple)
Out[8]:
int

Python assumes that (1) is an expression - 1 - inside parentheses - like this:

In [9]:
a_number = (1 + 2 + 3)
a_number
Out[9]:
6

If you want a single element tuple, you need to add a comma to tell Python that is what you meant:

In [10]:
a_tuple = (1,)
a_tuple
Out[10]:
(1,)

There is no such ambiguity for lists (square brackets don't enclose expressions):

In [11]:
a_list = [1]
a_list
Out[11]:
[1]

List comprehensions

List comprehensions are a nice short-cut for simple for loops in Python.

The list comprehension is a single expression that returns a list, element by element.

Let's say you wanted to create a list of values for squared numbers. You might do it like this:

In [12]:
squared_numbers = []
for i in range(10):  # numbers 0 through 9
    squared_numbers.append(i ** 2)
squared_numbers
Out[12]:
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

It turns out this kind of thing is a very common pattern in Python. That is, create an empty list, then use a for loop to fill in values for the list.

List comprehensions are a short cut for that pattern.

In [13]:
squared_numbers = [i ** 2 for i in range(10)]
squared_numbers
Out[13]:
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

The list comprehesion is an expression, starting and ending with square brackets. The first thing inside the square brackets is the expression that will become the element of the list - in this case i ** 2 - followed by a for clause - in this case for in in range(10) - that will feed the first expression with values to use.

See the Python docs on list comprehensions for a little more detail.

List comprehensions can be a little hard to read at first. If you find them confusing, as most of us do at first, then unpack them into the equivalent for loop. Over time, as you get used to them, they can be easier to read than the longer for loop equivalent.

We'll practice list comprehensions more over the next few classes.

Packages and namespaces

We have already seen examples of importing a package.

In [14]:
import numpy as np

This Python code imports the module called numpy and gives it the new name np.

What type() of thing is np now?

In [15]:
type(np)
Out[15]:
module

A module contains stuff with names. For example, numpy contains a function named sqrt:

In [16]:
np.sqrt?

Because a module contains stuff with names, it is a namespace.

Numpy is also a package because it is a set of modules that get installed together. For example, after you have installed numpy, you not only have the numpy module, but other submodules such as numpy.linalg, for linear algebra on numpy arrays:

In [17]:
import numpy.linalg as npl
type(npl)
Out[17]:
module

Try tab completing on npl. (npl followed by a period - .) to see what is in there.

Numpy is the module that contains the basic routines for creating and working with 1D and 2D and 3D - and in fact ND arrays in Python. Almost every scientific computing package in Python uses numpy.

We will be using two other packages for the exercises - matplotlib and nibabel. Both of these packages depend heavily on numpy for working with arrays.

Matplotlib is the standard Python package for doing high-quality plots. The original author wrote matplotlib to be similar to MATLAB (hence the name).

The best module for standard use is matplotlib.pyplot and we will import it like this:

In [18]:
import matplotlib.pyplot as plt
In [19]:
type(plt)
Out[19]:
module

Because we are using matplotlib in the IPython notebook, we also run an IPython command to make Matplotlib plots appear inside the notebook rather than putting up the plots as separate windows:

In [20]:
%matplotlib inline

Lastly, we will be using the nibabel package for loading neuroimaging format images:

In [21]:
import nibabel as nib

Getting help

If you don't know how to do a particular task in Python / Numpy / Matplotlib, then try these steps:

  • In IPython, do tab completion in the module, and have a look around. For example, if you are looking for a routine to do rounding on arrays, then type np.ro followed by and you will see np.round as one of the suggestions;
  • In IPython, get the help for particular functions or classes with the question mark at the end of the function or class name - e.g. np.round? followed by in the notebook.
  • In numpy or scipy (we'll come across scipy later), you can find stuff using lookfor. For example, let's say you hadn't guessed that np.sqrt was the square root function in numpy, you could try np.lookfor('square root').
  • Do a web search : http://lmgtfy.com/?q=numpy+square+root

Don't forget to ask your instructor - we live to help you acquire Python expertise.

Arrays as images, images as arrays

The main point of this class is to show that you can consider arrays as images, and images as arrays.

Let's make an array of numbers between 0 through 99:

In [22]:
an_array = np.array([[ 0,  0,  0,  0,  0,  0,  0,  0],
                     [ 0,  0,  0,  9, 99, 99, 94,  0],
                     [ 0,  0,  0, 25, 99, 99, 79,  0],
                     [ 0,  0,  0,  0,  0,  0,  0,  0],
                     [ 0,  0,  0, 56, 99, 99, 49,  0],
                     [ 0,  0,  0, 73, 99, 99, 31,  0],
                     [ 0,  0,  0, 91, 99, 99, 13,  0],
                     [ 0,  0,  9, 99, 99, 94,  0,  0],
                     [ 0,  0, 27, 99, 99, 77,  0,  0],
                     [ 0,  0, 45, 99, 99, 59,  0,  0],
                     [ 0,  0, 63, 99, 99, 42,  0,  0],
                     [ 0,  0, 80, 99, 99, 24,  0,  0],
                     [ 0,  1, 96, 99, 99,  6,  0,  0],
                     [ 0, 16, 99, 99, 88,  0,  0,  0],
                     [ 0,  0,  0,  0,  0,  0,  0,  0]])
an_array.shape
Out[22]:
(15, 8)

In fact this array represents a monochrome picture of a letter.

We can show arrays as images using the plt.imshow command. This is the default output:

In [23]:
plt.imshow(an_array)
Out[23]:
<matplotlib.image.AxesImage at 0x107ceae90>

The image looks rather blurry. This is because matplotlib is drawing an image with many more pixels than the array has values. For the pixels in-between array values, matplotlib is using interpolation to estimate a good value. We will hear a lot more about interpolation in later classes. For now, we turn off interpolation like this:

In [24]:
plt.imshow(an_array, interpolation='nearest')
Out[24]:
<matplotlib.image.AxesImage at 0x107ef6a90>

The image is weirdly colorful. That is because matplotlib is using the default colormap. A colormap is a mapping from values in the array to colors. In this case the default colormap is called jet and maps low numbers in the image (0 in our case) to blue, and high numbers (99 in our case) to red.

We can see what the colormap is doing by asking for a color bar:

In [25]:
plt.imshow(an_array, interpolation='nearest')
plt.colorbar()
Out[25]:
<matplotlib.colorbar.Colorbar instance at 0x108179170>

In our case, our image would make more sense as grayscale, so we use the gray colormap, like this:

In [26]:
plt.imshow(an_array, interpolation='nearest', cmap='gray')
plt.colorbar()
Out[26]:
<matplotlib.colorbar.Colorbar instance at 0x10833c680>

A grayscale image is an array containing numbers giving the pixel intensity values - in our case between 0 and 99.

We can also plot lines in matplotlib. For example, we might want to plot row 8 out of this array (the 9th row):

In [27]:
plt.plot(an_array[8])
Out[27]:
[<matplotlib.lines.Line2D at 0x1086920d0>]

The x axis is the position in the array (0 through 7) and the y axis is the value of the array row at that position.

The plot shows us the 0 values at the edges of the bar of the "i", an the ramp up to the peak at the middle of the bar of the "i", in columns number 3 and 4.

A transpose in numpy uses the .T method on the array. This has the effect of flipping the rows and columns (in 2D):

In [28]:
an_array.T
Out[28]:
array([[ 0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0],
       [ 0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  1, 16,  0],
       [ 0,  0,  0,  0,  0,  0,  0,  9, 27, 45, 63, 80, 96, 99,  0],
       [ 0,  9, 25,  0, 56, 73, 91, 99, 99, 99, 99, 99, 99, 99,  0],
       [ 0, 99, 99,  0, 99, 99, 99, 99, 99, 99, 99, 99, 99, 88,  0],
       [ 0, 99, 99,  0, 99, 99, 99, 94, 77, 59, 42, 24,  6,  0,  0],
       [ 0, 94, 79,  0, 49, 31, 13,  0,  0,  0,  0,  0,  0,  0,  0],
       [ 0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0]])
In [29]:
plt.imshow(an_array.T, interpolation='nearest', cmap='gray')
Out[29]:
<matplotlib.image.AxesImage at 0x108784dd0>

We can also reshape the original array to a 1D array, by stacking all the rows end to end:

In [30]:
a_1d_array = np.reshape(an_array, an_array.shape[0] * an_array.shape[1])
a_1d_array
Out[30]:
array([ 0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  9, 99, 99, 94,  0,  0,
        0,  0, 25, 99, 99, 79,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,
        0, 56, 99, 99, 49,  0,  0,  0,  0, 73, 99, 99, 31,  0,  0,  0,  0,
       91, 99, 99, 13,  0,  0,  0,  9, 99, 99, 94,  0,  0,  0,  0, 27, 99,
       99, 77,  0,  0,  0,  0, 45, 99, 99, 59,  0,  0,  0,  0, 63, 99, 99,
       42,  0,  0,  0,  0, 80, 99, 99, 24,  0,  0,  0,  1, 96, 99, 99,  6,
        0,  0,  0, 16, 99, 99, 88,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,
        0])
In [31]:
a_1d_array.shape
Out[31]:
(120,)

Reshaping the array to one dimension is a common operation, so there is a separate numpy command for that, np.ravel:

In [32]:
np.ravel(an_array)
Out[32]:
array([ 0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  9, 99, 99, 94,  0,  0,
        0,  0, 25, 99, 99, 79,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,
        0, 56, 99, 99, 49,  0,  0,  0,  0, 73, 99, 99, 31,  0,  0,  0,  0,
       91, 99, 99, 13,  0,  0,  0,  9, 99, 99, 94,  0,  0,  0,  0, 27, 99,
       99, 77,  0,  0,  0,  0, 45, 99, 99, 59,  0,  0,  0,  0, 63, 99, 99,
       42,  0,  0,  0,  0, 80, 99, 99, 24,  0,  0,  0,  1, 96, 99, 99,  6,
        0,  0,  0, 16, 99, 99, 88,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,
        0])

One use of the 1D version of the array, is for getting a histogram of the distribution of values in the array:

In [33]:
plt.hist(a_1d_array)
Out[33]:
(array([ 75.,   2.,   3.,   1.,   3.,   2.,   1.,   3.,   2.,  28.]),
 array([  0. ,   9.9,  19.8,  29.7,  39.6,  49.5,  59.4,  69.3,  79.2,
         89.1,  99. ]),
 <a list of 10 Patch objects>)

By default, the plt.hist function uses 50 bins, but you can specify how many bins you want with the bins keyword:

In [34]:
plt.hist(a_1d_array, bins=75)
Out[34]:
(array([ 72.,   0.,   0.,   0.,   1.,   0.,   2.,   0.,   0.,   1.,   0.,
          0.,   1.,   0.,   0.,   0.,   0.,   0.,   2.,   0.,   1.,   0.,
          0.,   1.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   1.,   0.,
          0.,   1.,   0.,   0.,   1.,   0.,   0.,   0.,   0.,   1.,   0.,
          1.,   0.,   0.,   1.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,
          1.,   0.,   0.,   1.,   1.,   1.,   0.,   0.,   0.,   0.,   0.,
          1.,   0.,   1.,   0.,   0.,   2.,   1.,   0.,  24.]),
 array([  0.  ,   1.32,   2.64,   3.96,   5.28,   6.6 ,   7.92,   9.24,
         10.56,  11.88,  13.2 ,  14.52,  15.84,  17.16,  18.48,  19.8 ,
         21.12,  22.44,  23.76,  25.08,  26.4 ,  27.72,  29.04,  30.36,
         31.68,  33.  ,  34.32,  35.64,  36.96,  38.28,  39.6 ,  40.92,
         42.24,  43.56,  44.88,  46.2 ,  47.52,  48.84,  50.16,  51.48,
         52.8 ,  54.12,  55.44,  56.76,  58.08,  59.4 ,  60.72,  62.04,
         63.36,  64.68,  66.  ,  67.32,  68.64,  69.96,  71.28,  72.6 ,
         73.92,  75.24,  76.56,  77.88,  79.2 ,  80.52,  81.84,  83.16,
         84.48,  85.8 ,  87.12,  88.44,  89.76,  91.08,  92.4 ,  93.72,
         95.04,  96.36,  97.68,  99.  ]),
 <a list of 75 Patch objects>)

As you can imagine, it's easy to go back to the 2D shape, by splitting the 1D array back into 15 rows of 8 values each (and therefore 8 columns):

In [35]:
array_back = np.reshape(a_1d_array, (15, 8))
array_back
Out[35]:
array([[ 0,  0,  0,  0,  0,  0,  0,  0],
       [ 0,  0,  0,  9, 99, 99, 94,  0],
       [ 0,  0,  0, 25, 99, 99, 79,  0],
       [ 0,  0,  0,  0,  0,  0,  0,  0],
       [ 0,  0,  0, 56, 99, 99, 49,  0],
       [ 0,  0,  0, 73, 99, 99, 31,  0],
       [ 0,  0,  0, 91, 99, 99, 13,  0],
       [ 0,  0,  9, 99, 99, 94,  0,  0],
       [ 0,  0, 27, 99, 99, 77,  0,  0],
       [ 0,  0, 45, 99, 99, 59,  0,  0],
       [ 0,  0, 63, 99, 99, 42,  0,  0],
       [ 0,  0, 80, 99, 99, 24,  0,  0],
       [ 0,  1, 96, 99, 99,  6,  0,  0],
       [ 0, 16, 99, 99, 88,  0,  0,  0],
       [ 0,  0,  0,  0,  0,  0,  0,  0]])
In [36]:
plt.imshow(array_back, interpolation='nearest', cmap='gray')
Out[36]:
<matplotlib.image.AxesImage at 0x108f0d890>

In numpy, basic operations like multiplication, addition, comparison, are always elementwise. For example, this multiplies every array value by 10:

In [37]:
an_array * 10
Out[37]:
array([[  0,   0,   0,   0,   0,   0,   0,   0],
       [  0,   0,   0,  90, 990, 990, 940,   0],
       [  0,   0,   0, 250, 990, 990, 790,   0],
       [  0,   0,   0,   0,   0,   0,   0,   0],
       [  0,   0,   0, 560, 990, 990, 490,   0],
       [  0,   0,   0, 730, 990, 990, 310,   0],
       [  0,   0,   0, 910, 990, 990, 130,   0],
       [  0,   0,  90, 990, 990, 940,   0,   0],
       [  0,   0, 270, 990, 990, 770,   0,   0],
       [  0,   0, 450, 990, 990, 590,   0,   0],
       [  0,   0, 630, 990, 990, 420,   0,   0],
       [  0,   0, 800, 990, 990, 240,   0,   0],
       [  0,  10, 960, 990, 990,  60,   0,   0],
       [  0, 160, 990, 990, 880,   0,   0,   0],
       [  0,   0,   0,   0,   0,   0,   0,   0]])

Comparison is also elementwise. For example, this gives True for every value > 50, and False for every value <= 50:

In [38]:
an_array > 50
Out[38]:
array([[False, False, False, False, False, False, False, False],
       [False, False, False, False,  True,  True,  True, False],
       [False, False, False, False,  True,  True,  True, False],
       [False, False, False, False, False, False, False, False],
       [False, False, False,  True,  True,  True, False, False],
       [False, False, False,  True,  True,  True, False, False],
       [False, False, False,  True,  True,  True, False, False],
       [False, False, False,  True,  True,  True, False, False],
       [False, False, False,  True,  True,  True, False, False],
       [False, False, False,  True,  True,  True, False, False],
       [False, False,  True,  True,  True, False, False, False],
       [False, False,  True,  True,  True, False, False, False],
       [False, False,  True,  True,  True, False, False, False],
       [False, False,  True,  True,  True, False, False, False],
       [False, False, False, False, False, False, False, False]], dtype=bool)

Matplotlib will treat False as 0 and True as 1, so this is one way of binarizing the image at a threshold (of 50 in this case):

In [39]:
plt.imshow(an_array > 50, interpolation='nearest', cmap='gray')
Out[39]:
<matplotlib.image.AxesImage at 0x108fc3dd0>

We can slice arrays as we slice strings or lists. The difference for arrays is that we can slice in any or all dimensions at the same time. For example, to get the dot of the "i" it looks (from the numbers at the sides of the ploat) that we want to the top 4 rows, and the last 5 columns:

In [40]:
an_array[0:4, 3:]
Out[40]:
array([[ 0,  0,  0,  0,  0],
       [ 9, 99, 99, 94,  0],
       [25, 99, 99, 79,  0],
       [ 0,  0,  0,  0,  0]])
In [41]:
plt.imshow(an_array[0:4, 3:], interpolation='nearest', cmap='gray')
Out[41]:
<matplotlib.image.AxesImage at 0x1090ba550>