From 2c905e2cb694ffc30bf2410c2696acee81e1ebae Mon Sep 17 00:00:00 2001
From: Paul McCarthy <pauldmccarthy@gmail.com>
Date: Thu, 1 Feb 2018 14:40:32 +0000
Subject: [PATCH] I think numpy is finished

---
 getting_started/04_numpy.ipynb | 559 ++++++++++++++++++++++++++++-----
 getting_started/04_numpy.md    |  26 +-
 2 files changed, 497 insertions(+), 88 deletions(-)

diff --git a/getting_started/04_numpy.ipynb b/getting_started/04_numpy.ipynb
index 545ae75..e877863 100644
--- a/getting_started/04_numpy.ipynb
+++ b/getting_started/04_numpy.ipynb
@@ -19,28 +19,40 @@
     "alternative to Matlab as a scientific computing platform.\n",
     "\n",
     "\n",
+    "The `fslpython` environment currently includes [Numpy\n",
+    "1.11.1](https://docs.scipy.org/doc/numpy-1.11.0/index.html), which is a little\n",
+    "out of date, but we will update it for the next release of FSL.\n",
+    "\n",
+    "\n",
     "## Contents\n",
     "\n",
     "\n",
     "* [The Python list versus the Numpy array](#the-python-list-versus-the-numpy-array)\n",
     "* [Numpy basics](#numpy-basics)\n",
     " * [Creating arrays](#creating-arrays)\n",
-    " * [Operating on arrays](#operating-on-arrays)\n",
+    " * [Loading text files](#loading-text-files)\n",
     " * [Array properties](#array-properties)\n",
     " * [Descriptive statistics](#descriptive-statistics)\n",
     " * [Reshaping and rearranging arrays](#reshaping-and-rearranging-arrays)\n",
-    "* [Multi-variate operations](#multi-variate-operations)\n",
+    "* [Operating on arrays](#operating-on-arrays)\n",
+    " * [Scalar operations](#scalar-operations)\n",
+    " * [Multi-variate operations](#multi-variate-operations)\n",
     " * [Matrix multplication](#matrix-multiplication)\n",
     " * [Broadcasting](#broadcasting)\n",
+    " * [Linear algebra](#linear-algebra)\n",
     "* [Array indexing](#array-indexing)\n",
     " * [Indexing multi-dimensional arrays](#indexing-multi-dimensional-arrays)\n",
     " * [Boolean indexing](#boolean-indexing)\n",
     " * [Coordinate array indexing](#coordinate-array-indexing)\n",
-    "* [Generating random numbers](#generating-random-numbers)\n",
+    "* [Exercises](#exercises)\n",
+    " * [Load an array from a file and do stuff with it](#load-an-array-from-a-file-and-do-stuff-with-it)\n",
+    " * [Concatenate affine transforms](#concatenate-affine-transforms)\n",
     "\n",
-    "* [Appendix: Importing Numpy](#appendix-importing-numpy)\n",
-    "* [Appendix: Vectors in Numpy](#appendix-vectors-in-numpy)\n",
+    "* [Appendix A: Generating random numbers](#appendix-generating-random-numbers)\n",
+    "* [Appendix B: Importing Numpy](#appendix-importing-numpy)\n",
+    "* [Appendix C: Vectors in Numpy](#appendix-vectors-in-numpy)\n",
     "\n",
+    "* [Useful references](#useful-references)\n",
     "\n",
     "\n",
     "<a class=\"anchor\" id=\"the-python-list-versus-the-numpy-array\"></a>\n",
@@ -98,13 +110,12 @@
     "computing!\n",
     "\n",
     "\n",
-    "This is a major source of confusion for those poor souls who have spent their\n",
-    "lives working in Matlab, but have finally seen the light and switched to\n",
-    "Python. It is _crucial_ to be able to distinguish between a Python list and a\n",
-    "Numpy array.\n",
+    "This is a major source of confusion for people who are learning Python, and\n",
+    "are trying to write efficient code. It is _crucial_ to be able to distinguish\n",
+    "between a Python list and a Numpy array.\n",
     "\n",
     "\n",
-    "___Python list == Matlab cell array:___ A list in python is akin to a cell\n",
+    "___Python list == Matlab cell array:___ A list in Python is akin to a cell\n",
     "array in Matlab - they can store anything, but are extremely inefficient, and\n",
     "unwieldy when you have more than a couple of dimensions.\n",
     "\n",
@@ -216,7 +227,7 @@
     "print('np.ones gives us ones:                         ', np.ones(5))\n",
     "print('np.arange gives us a range:                    ', np.arange(5))\n",
     "print('np.linspace gives us N linearly spaced numbers:', np.linspace(0, 1, 5))\n",
-    "print('np.random.random gives us random numbers:      ', np.random.random(5))\n",
+    "print('np.random.random gives us random numbers [0-1]:', np.random.random(5))\n",
     "print('np.random.randint gives us random integers:    ', np.random.randint(1, 10, 5))\n",
     "print('np.eye gives us an identity matrix:')\n",
     "print(np.eye(4))\n",
@@ -228,7 +239,8 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "> There will be more on random numbers [below](#generating-random-numbers).\n",
+    "> See the [appendix](#appendix-generating-random-numbers) for more information\n",
+    "> on generating random numbers in Numpy.\n",
     "\n",
     "\n",
     "The `zeros` and `ones` functions can also be used to generate N-dimensional\n",
@@ -255,12 +267,12 @@
     "> second to columns - just like in Matlab.\n",
     "\n",
     "\n",
-    "<a class=\"anchor\" id=\"operating-on-arrays\"></a>\n",
-    "### Operating on arrays\n",
+    "<a class=\"anchor\" id=\"loading-text-files\"></a>\n",
+    "### Loading text files\n",
     "\n",
     "\n",
-    "All of the mathematical operators you know and love can be applied to Numpy\n",
-    "arrays:"
+    "The `numpy.loadtxt` function is capable of loading numerical data from\n",
+    "plain-text files. By default it expects space-separated data:"
    ]
   },
   {
@@ -269,15 +281,55 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "a = np.random.randint(1, 10, (3, 3))\n",
-    "print('a:')\n",
-    "print(a)\n",
-    "print('a + 2:')\n",
-    "print( a + 2)\n",
-    "print('a * 3:')\n",
-    "print( a * 3)\n",
-    "print('a % 2:')\n",
-    "print( a % 2)"
+    "data = np.loadtxt('04_numpy/space_separated.txt')\n",
+    "print('data in 04_numpy/space_separated.txt:')\n",
+    "print(data)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "But you can also specify the delimiter to expect<sup>1</sup>:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "data = np.loadtxt('04_numpy/comma_separated.txt', delimiter=',')\n",
+    "print('data in 04_numpy/comma_separated.txt:')\n",
+    "print(data)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "> <sup>1</sup> And many other things such as file headers, footers, comments,\n",
+    "> and newline characters - see the\n",
+    "> [docs](https://docs.scipy.org/doc/numpy/reference/generated/numpy.loadtxt.html)\n",
+    "> for more information.\n",
+    "\n",
+    "\n",
+    "  Of course you can also save data out to a text file just as easily, with\n",
+    "[`numpy.savetxt`](https://docs.scipy.org/doc/numpy/reference/generated/numpy.savetxt.html):"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "data = np.random.randint(1, 10, (10, 10))\n",
+    "np.savetxt('mydata.txt', data, delimiter=',', fmt='%i')\n",
+    "\n",
+    "with open('mydata.txt', 'rt') as f:\n",
+    "    for line in f:\n",
+    "        print(line.strip())"
    ]
   },
   {
@@ -323,8 +375,8 @@
     "### Descriptive statistics\n",
     "\n",
     "\n",
-    "Similarly, a Numpy array has a set of methods<sup>1</sup> which allow you to\n",
-    "calculate basic descriptive statisics on an array:"
+    "Similarly, a Numpy array has a set of methods<sup>2</sup> which allow you to\n",
+    "calculate basic descriptive statistics on an array:"
    ]
   },
   {
@@ -350,11 +402,12 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "> <sup>1</sup> Python, being an object-oriented language, distinguishes\n",
-    "> between _functions_ and _methods_. _Method_ is simply the term used to refer\n",
-    "> to a function that is associated with a specific object. Similarly, the term\n",
-    "> _attribute_ is used to refer to some piece of information that is attached\n",
-    "> to an object, such as `z.shape`, or `z.dtype`.\n",
+    "> <sup>2</sup> Python, being an object-oriented language, distinguishes\n",
+    "> between _functions_ and _methods_. Hopefully we all know what a function is\n",
+    "> - a _method_ is simply the term used to refer to a function that is\n",
+    "> associated with a specific object. Similarly, the term _attribute_ is used\n",
+    "> to refer to some piece of information that is attached to an object, such as\n",
+    "> `z.shape`, or `z.dtype`.\n",
     "\n",
     "\n",
     "<a class=\"anchor\" id=\"reshaping-and-rearranging-arrays\"></a>\n",
@@ -370,7 +423,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "a = np.random.randint(1, 10, (4, 4))\n",
+    "a = np.arange(16).reshape(4, 4)\n",
     "b = a.reshape((2, 8))\n",
     "print('a:')\n",
     "print(a)\n",
@@ -414,7 +467,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "a = np.random.randint(1, 10, (4, 4))\n",
+    "a = np.arange(16).reshape((4, 4))\n",
     "b = np.array(a.reshape(2, 8))\n",
     "a[3, 3] = 12345\n",
     "b[0, 7] = 54321\n",
@@ -437,7 +490,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "a = np.random.randint(1, 10, (4, 4))\n",
+    "a = np.arange(16).reshape((4, 4))\n",
     "print(a)\n",
     "print(a.T)"
    ]
@@ -447,7 +500,7 @@
    "metadata": {},
    "source": [
     "The `transpose` method allows you to obtain more complicated rearrangements\n",
-    "of an array's axes:"
+    "of an N-dimensional array's axes:"
    ]
   },
   {
@@ -456,7 +509,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "a = np.random.randint(1, 10, (2, 3, 4))\n",
+    "a = np.arange(24).reshape((2, 3, 4))\n",
     "b = a.transpose((2, 0, 1))\n",
     "print('a: ', a.shape)\n",
     "print(a)\n",
@@ -533,15 +586,53 @@
     "print( dstacked)"
    ]
   },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "<a class=\"anchor\" id=\"operating-on-arrays\"></a>\n",
+    "## Operating on arrays\n",
+    "\n",
+    "\n",
+    "If you are coming from Matlab, you should read this section as, while many\n",
+    "Numpy operations behave similarly to Matlab, there are a few important\n",
+    "behaviours which differ from what you might expect.\n",
+    "\n",
+    "\n",
+    "<a class=\"anchor\" id=\"scalar-operations\"></a>\n",
+    "### Scalar operations\n",
+    "\n",
+    "\n",
+    "All of the mathematical operators you know and love can be applied to Numpy\n",
+    "arrays:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "a = np.arange(1, 10).reshape((3, 3))\n",
+    "print('a:')\n",
+    "print(a)\n",
+    "print('a + 2:')\n",
+    "print( a + 2)\n",
+    "print('a * 3:')\n",
+    "print( a * 3)\n",
+    "print('a % 2:')\n",
+    "print( a % 2)"
+   ]
+  },
   {
    "cell_type": "markdown",
    "metadata": {},
    "source": [
     "<a class=\"anchor\" id=\"multi-variate-operations\"></a>\n",
-    "## Multi-variate operations\n",
+    "### Multi-variate operations\n",
     "\n",
     "\n",
-    "Many operations in Numpy operate on an elementwise basis. For example:"
+    "Many operations in Numpy operate on an element-wise basis. For example:"
    ]
   },
   {
@@ -550,8 +641,8 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "a = np.random.randint(1, 10, (5))\n",
-    "b = np.random.randint(1, 10, (5))\n",
+    "a = np.ones(5)\n",
+    "b = np.random.randint(1, 10, 5)\n",
     "\n",
     "print('a:     ', a)\n",
     "print('b:     ', b)\n",
@@ -572,8 +663,8 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "a = np.random.randint(1, 10, (4, 4))\n",
-    "b = np.random.randint(1, 10, (4, 4))\n",
+    "a = np.ones((4, 4))\n",
+    "b = np.arange(16).reshape((4, 4))\n",
     "\n",
     "print('a:')\n",
     "print(a)\n",
@@ -593,15 +684,17 @@
     "Wait ... what's that you say? Oh, I couldn't understand because of all the\n",
     "froth coming out of your mouth. I guess you're angry that `a * b` didn't give\n",
     "you the matrix product, like it would have in Matlab.  Well all I can say is\n",
-    "that Python is not Matlab. Get over it. Take a calmative.\n",
+    "that Numpy is not Matlab. Matlab operations are typically consistent with\n",
+    "linear algebra notation. This is not the case in Numpy. Get over it. Take a\n",
+    "calmative.\n",
     "\n",
     "\n",
     "<a class=\"anchor\" id=\"matrix-multiplication\"></a>\n",
-    "*## Matrix multiplication\n",
+    "### Matrix multiplication\n",
     "\n",
     "\n",
     "When your heart rate has returned to its normal caffeine-induced state, you\n",
-    "can use the `dot` method, or the `@` operator, to perform matrix\n",
+    "can use the `@` operator or the `dot` method, to perform matrix\n",
     "multiplication:"
    ]
   },
@@ -611,8 +704,8 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "a = np.random.randint(1, 10, (4, 4))\n",
-    "b = np.random.randint(1, 10, (4, 4))\n",
+    "a = np.arange(1, 5).reshape((2, 2))\n",
+    "b = a.T\n",
     "\n",
     "print('a:')\n",
     "print(a)\n",
@@ -633,24 +726,136 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "> The `@` matrix multiplication operator is a relatively recent addition\n",
-    "> to Python and Numpy, so you might not see it all that often in existing\n",
-    "> code. But it's here to stay, so go ahead and use it!\n",
+    "> The `@` matrix multiplication operator is a relatively recent addition to\n",
+    "> Python and Numpy, so you might not see it all that often in existing\n",
+    "> code. But it's here to stay, so if you don't need to worry about\n",
+    "> backwards-compatibility, go ahead and use it!\n",
     "\n",
     "\n",
     "<a class=\"anchor\" id=\"broadcasting\"></a>\n",
     "### Broadcasting\n",
     "\n",
     "\n",
-    "One of the coolest (and possibly confusing) features of Numpy is its\n",
-    "[_broadcasting_\n",
-    "rules](https://docs.scipy.org/doc/numpy/user/basics.broadcasting.html).\n",
+    "One of the coolest features of Numpy is _broadcasting_<sup>3</sup>.\n",
+    "Broadcasting allows you to perform element-wise operations on arrays which\n",
+    "have a different shape. For each axis in the two arrays, Numpy will implicitly\n",
+    "expand the shape of the smaller axis to match the shape of the larger one. You\n",
+    "never need to use `repmat` ever again!\n",
     "\n",
     "\n",
+    "> <sup>3</sup>Mathworks have shamelessly stolen Numpy's broadcasting behaviour\n",
+    "> and included it in Matlab versions from 2016b onwards, referring to it as\n",
+    "> _implicit expansion_.\n",
+    "\n",
+    "\n",
+    "Broadcasting allows you to, for example, add the elements of a 1D vector to\n",
+    "all of the rows or columns of a 2D array:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "a = np.arange(9).reshape((3, 3))\n",
+    "b = np.arange(1, 4)\n",
+    "print('a:')\n",
+    "print(a)\n",
+    "print('b: ', b)\n",
+    "print('a * b (row-wise broadcasting):')\n",
+    "print(a * b)\n",
+    "print('a * b.T (column-wise broadcasting):')\n",
+    "print(a * b.reshape(-1, 1))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "> Here we used a handy feature of the `reshape` method - if you pass `-1` for\n",
+    "> the size of one dimension, it will automatically determine the size to use\n",
+    "> for that dimension. Take a look at [the\n",
+    "> appendix](#appendix-vectors-in-numpy) for a discussion on vectors in Numpy.\n",
+    "\n",
+    "\n",
+    "Here is a more useful example, where we use broadcasting to de-mean the rows\n",
+    "or columns of an array:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "a = np.arange(9).reshape((3, 3))\n",
+    "print('a:')\n",
+    "print(a)\n",
+    "\n",
+    "print('a (cols demeaned):')\n",
+    "print(a - a.mean(axis=0))\n",
+    "\n",
+    "print('a (rows demeaned):')\n",
+    "print(a - a.mean(axis=1).reshape(-1, 1))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "> As demonstrated above, many functions in Numpy accept an `axis` parameter,\n",
+    "> allowing you to apply the function along a specific axis. Omitting the\n",
+    "> `axis` parameter will apply the function to the whole array.\n",
+    "\n",
+    "\n",
+    "Broadcasting can sometimes be confusing, but the rules which Numpy follows to\n",
+    "align arrays of different sizes, and hence determine how the broadcasting\n",
+    "should be applied, are pretty straightforward. If something is not working,\n",
+    "and you can't figure out why refer to the [official\n",
+    "documentation](https://docs.scipy.org/doc/numpy/user/basics.broadcasting.html).\n",
+    "\n",
+    "\n",
+    "<a class=\"anchor\" id=\"linear-algebra\"></a>\n",
+    "### Linear algebra\n",
+    "\n",
+    "\n",
+    "Numpy is first and foremost a library for general-purpose numerical computing.\n",
+    "But it does have a range of linear algebra functionality, hidden away in the\n",
+    "[`numpy.linalg`](https://docs.scipy.org/doc/numpy/reference/routines.linalg.html)\n",
+    "module. Here are a couple of quick examples:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import numpy.linalg as npla\n",
+    "\n",
+    "a = np.array([[1, 2, 3,  4],\n",
+    "              [0, 5, 6,  7],\n",
+    "              [0, 0, 8,  9],\n",
+    "              [0, 0, 0, 10]])\n",
     "\n",
+    "print('a:')\n",
+    "print(a)\n",
     "\n",
+    "print('inv(a)')\n",
+    "print(npla.inv(a))\n",
     "\n",
+    "eigvals, eigvecs = npla.eig(a)\n",
     "\n",
+    "print('eigenvalues and vectors of a:')\n",
+    "for val, vec in zip(eigvals, eigvecs):\n",
+    "    print('{:2.0f} - {}'.format(val, vec))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
     "<a class=\"anchor\" id=\"array-indexing\"></a>\n",
     "## Array indexing\n",
     "\n",
@@ -666,8 +871,10 @@
     "\n",
     "\n",
     "Let's whet our appetites with some basic 1D array slicing. Numpy supports the\n",
-    "standard Python __slice__ notation for indexing, where you can specify the\n",
-    "start and end indices, and the step size, via the `start:stop:step` syntax:"
+    "standard Python\n",
+    "[__slice__](https://www.pythoncentral.io/how-to-slice-listsarrays-and-tuples-in-python/)\n",
+    "notation for indexing, where you can specify the start and end indices, and\n",
+    "the step size, via the `start:stop:step` syntax:"
    ]
   },
   {
@@ -676,7 +883,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "a = np.random.randint(1, 10, 10)\n",
+    "a = np.arange(10)\n",
     "\n",
     "print('a:                              ', a)\n",
     "print('first element:                  ', a[0])\n",
@@ -685,8 +892,8 @@
     "print('last element again:             ', a[-1])\n",
     "print('last two elements:              ', a[-2:])\n",
     "print('middle four elements:           ', a[3:7])\n",
-    "print('Every second element:           ', a[::2])\n",
-    "print('Every second element, reversed: ', a[1::-2])"
+    "print('Every second element:           ', a[1::2])\n",
+    "print('Every second element, reversed: ', a[-1::-2])"
    ]
   },
   {
@@ -703,7 +910,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "a = np.random.randint(1, 10, 10)\n",
+    "a = np.arange(10)\n",
     "print('a:', a)\n",
     "every2nd = a[::2]\n",
     "print('every 2nd:', every2nd)\n",
@@ -720,7 +927,8 @@
     "\n",
     "\n",
     "Multi-dimensional array indexing works in much the same way as one-dimensional\n",
-    "indexing but with, well, more dimensions:"
+    "indexing but with, well, more dimensions. Use commas within the square\n",
+    "brackets to separate the slices for each dimension:"
    ]
   },
   {
@@ -729,7 +937,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "a = np.random.randint(1, 10, (5, 5))\n",
+    "a = np.arange(25).reshape((5, 5))\n",
     "print('a:')\n",
     "print(a)\n",
     "print(' First row:     ', a[  0, :])\n",
@@ -754,7 +962,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "a = np.random.randint(1, 10, (3, 3, 3))\n",
+    "a = np.arange(27).reshape((3, 3, 3))\n",
     "print('a:')\n",
     "print(a)\n",
     "print('All elements at x=0:')\n",
@@ -783,7 +991,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "a = np.random.randint(1, 10, 10)\n",
+    "a = np.arange(10)\n",
     "\n",
     "print('a:                          ', a)\n",
     "print('a > 5:                      ', a > 4)\n",
@@ -804,7 +1012,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "a = np.random.randint(1, 10, 10)\n",
+    "a = np.arange(10)\n",
     "b = a[a > 5]\n",
     "print('a: ', a)\n",
     "print('b: ', b)\n",
@@ -837,19 +1045,43 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "a    = np.random.randint(1, 10, 10)\n",
+    "a    = np.arange(10)\n",
     "gt5  = a > 5\n",
     "even = a % 2 == 0\n",
     "\n",
     "print('a:                                    ', a)\n",
-    "print('elements in a which are > 5:          ', a[gt5])\n",
+    "print('elements in a which are > 5:          ', a[ gt5])\n",
     "print('elements in a which are <= 5:         ', a[~gt5])\n",
-    "print('elements in a which are even:         ', a[even])\n",
+    "print('elements in a which are even:         ', a[ even])\n",
     "print('elements in a which are odd:          ', a[~even])\n",
     "print('elements in a which are > 5 and even: ', a[gt5 &  even])\n",
     "print('elements in a which are > 5 or odd:   ', a[gt5 | ~even])"
    ]
   },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Numpy also has two handy functions, `all` and `any`, which respectively allow\n",
+    "you to perform boolean `and` and `or` operations along the axes of an array:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "a = np.arange(9).reshape((3, 3))\n",
+    "\n",
+    "print('a:')\n",
+    "print(a)\n",
+    "print('rows with any element divisible by 3: ', np.any(a % 3 == 0, axis=1))\n",
+    "print('cols with any element divisible by 3: ', np.any(a % 3 == 0, axis=0))\n",
+    "print('rows with all elements divisible by 3:', np.all(a % 3 == 0, axis=1))\n",
+    "print('cols with all elements divisible by 3:', np.all(a % 3 == 0, axis=0))"
+   ]
+  },
   {
    "cell_type": "markdown",
    "metadata": {},
@@ -861,7 +1093,7 @@
     "You can index a numpy array using another array containing coordinates into\n",
     "the first array.  As with boolean indexing, this will result in a copy of the\n",
     "data.  Generally, you will need to have a separate array, or list, of\n",
-    "coordinates into each data axis:"
+    "coordinates for each axis of the array you wish to index:"
    ]
   },
   {
@@ -870,7 +1102,8 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "a = np.random.randint(1, 10, (4, 4))\n",
+    "a = np.arange(16).reshape((4, 4))\n",
+    "print('a:')\n",
     "print(a)\n",
     "\n",
     "rows    = [0, 2, 3]\n",
@@ -885,12 +1118,167 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "<a class=\"anchor\" id=\"generating-random-numbers\"></a>\n",
-    "## Generating random numbers\n",
+    "The `numpy.where` function can be combined with boolean arrays to easily\n",
+    "generate of coordinate arrays for values which meet some condition:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "a = np.arange(16).reshape((4, 4))\n",
+    "print('a:')\n",
+    "print(a)\n",
+    "\n",
+    "evenrows, evencols = np.where(a % 2 == 0)\n",
+    "\n",
+    "print('even row coordinates:', evenx)\n",
+    "print('even col coordinates:', eveny)\n",
+    "\n",
+    "print(a[evenrows, evencols])"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "<a class=\"anchor\" id=\"exercises\"></a>\n",
+    "## Exercises\n",
+    "\n",
+    "\n",
+    "The challenge for each of these exercises is to complete them in as few lines\n",
+    "of code as possible!\n",
+    "\n",
+    "\n",
+    "> You can find example answers to the exercises in `04_numpy/.solutions`.\n",
+    "\n",
+    "\n",
+    "<a class=\"anchor\" id=\"load-an-array-from-a-file-and-do-stuff-with-it\"></a>\n",
+    "### Load an array from a file and do stuff with it\n",
+    "\n",
+    "\n",
+    "Load the file `04_numpy/2d_array.txt`, and calculate and print the mean for\n",
+    "each column.  If your code doesn't work, you might want to __LOOK AT YOUR\n",
+    "DATA__, as you will have learnt if you have ever attended the FSL course.\n",
+    "\n",
+    "\n",
+    "> Bonus: Find the hidden message (hint:\n",
+    "> [chr](https://docs.python.org/3/library/functions.html#chr))\n",
+    "\n",
+    "\n",
+    "<a class=\"anchor\" id=\"concatenate-affine-transforms\"></a>\n",
+    "### Concatenate affine transforms\n",
+    "\n",
+    "\n",
+    "Given all of the files in `04_numpy/xfms/`, create a transformation matrix\n",
+    "which can transform coordinates from subject 1 functional space to subject 2\n",
+    "functional space<sup>4</sup>.\n",
+    "\n",
+    "Write some code to transform the following coordinates from subject 1\n",
+    "functional space to subject 2 functional space, to test that your matrix is\n",
+    "correct:\n",
+    "\n",
+    "\n",
+    "| __Subject 1 coordinates__ | __Subject 2 coordinates__ |\n",
+    "|:-------------------------:|:-------------------------:|\n",
+    "| `[ 0.0,   0.0,   0.0]`    | `[ 3.21,   4.15, -9.89]`  |\n",
+    "| `[-5.0, -20.0,  10.0]`    | `[-0.87, -14.36, -1.13]`  |\n",
+    "| `[20.0,  25.0,  60.0]`    | `[29.58,  27.61, 53.37]`  |\n",
+    "\n",
+    "\n",
+    "> <sup>4</sup> Even though these are FLIRT transforms, this is just a toy\n",
+    "> example.  Look\n",
+    "> [here](https://fsl.fmrib.ox.ac.uk/fsl/fslwiki/FLIRT/FAQ#What_is_the_format_of_the_matrix_used_by_FLIRT.2C_and_how_does_it_relate_to_the_transformation_parameters.3F)\n",
+    "> and\n",
+    "> [here](https://git.fmrib.ox.ac.uk/fsl/fslpy/blob/1.6.2/fsl/utils/transform.py#L537)\n",
+    "> if you actually need to work with FLIRT transforms.\n",
+    "\n",
+    "\n",
+    "\n",
+    "<a class=\"anchor\" id=\"appendix-generating-random-numbers\"></a>\n",
+    "## Appendix A: Generating random numbers\n",
+    "\n",
+    "\n",
+    "Numpy's\n",
+    "[`numpy.random`](https://docs.scipy.org/doc/numpy/reference/routines.random.html)\n",
+    "module is where you should go if you want to introduce a little randomness\n",
+    "into your code.  You have already seen a couple of functions for generating\n",
+    "uniformly distributed real or integer data:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import numpy.random as npr\n",
+    "\n",
+    "print('Random floats between 0.0 (inclusive) and 1.0 (exclusive):')\n",
+    "print(npr.random((3, 3)))\n",
+    "\n",
+    "print('Random integers in a specified range:')\n",
+    "print(npr.randint(1, 100, (3, 3)))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "You can also draw random data from other distributions - here are just a few\n",
+    "examples:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "print('Gaussian (mean: 0, stddev: 1):')\n",
+    "print(npr.normal(0, 1, (3, 3)))\n",
     "\n",
+    "print('Gamma (shape: 1, scale: 1):')\n",
+    "print(npr.normal(1, 1, (3, 3)))\n",
     "\n",
+    "print('Chi-square (dof: 10):')\n",
+    "print(npr.chisquare(10, (3, 3)))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "The `numpy.random` module also has a couple of other handy functions for\n",
+    "random sampling of existing data:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "data = np.arange(5)\n",
+    "\n",
+    "print('data:               ', data)\n",
+    "print('two random values:  ', npr.choice(data, 2))\n",
+    "print('random permutation: ', npr.permutation(data))\n",
+    "\n",
+    "# The numpy.random.shuffle function\n",
+    "# will shuffle an array *in-place*.\n",
+    "npr.shuffle(data)\n",
+    "print('randomly shuffled: ', data)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
     "<a class=\"anchor\" id=\"appendix-importing-numpy\"></a>\n",
-    "## Appendix: Importing Numpy\n",
+    "## Appendix B: Importing Numpy\n",
     "\n",
     "\n",
     "For interactive exploration/experimentation, you might want to import\n",
@@ -934,7 +1322,7 @@
    "metadata": {},
    "source": [
     "But if you are writing a script or application using Numpy, I implore you to\n",
-    "Numpy like this instead:"
+    "Numpy (and its commonly used sub-modules) like this instead:"
    ]
   },
   {
@@ -943,7 +1331,9 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "import numpy as np"
+    "import numpy        as np\n",
+    "import numpy.random as npr\n",
+    "import numpy.linalg as npla"
    ]
   },
   {
@@ -963,10 +1353,12 @@
     "e = np.array([1, 2, 3, 4, 5])\n",
     "z = np.zeros((100, 100))\n",
     "d = np.diag([2, 3, 4, 5])\n",
+    "r = npr.random(5)\n",
     "\n",
     "print(e)\n",
     "print(z)\n",
-    "print(d)"
+    "print(d)\n",
+    "print(r)"
    ]
   },
   {
@@ -980,7 +1372,7 @@
     "\n",
     "\n",
     "<a class=\"anchor\" id=\"appendix-vectors-in-numpy\"></a>\n",
-    "## Appendix: Vectors in Numpy\n",
+    "## Appendix C: Vectors in Numpy\n",
     "\n",
     "\n",
     "One aspect of Numpy which might trip you up, and which can be quite\n",
@@ -1016,9 +1408,16 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "> Here we used a handy feature of the `reshape` method - if you pass `-1` for\n",
-    "> the size of one dimension, it will automatically determine the size to use\n",
-    "> for that dimension."
+    "<a class=\"anchor\" id=\"useful-references\"></a>\n",
+    "## Useful references\n",
+    "\n",
+    "\n",
+    "* [The Numpy manual](https://docs.scipy.org/doc/numpy/)\n",
+    "* [Linear algebra in `numpy.linalg`](https://docs.scipy.org/doc/numpy/reference/routines.linalg.html)\n",
+    "* [Broadcasting in Numpy](https://docs.scipy.org/doc/numpy/user/basics.broadcasting.html)\n",
+    "* [Indexing in Numpy](https://docs.scipy.org/doc/numpy/reference/arrays.indexing.html)\n",
+    "* [Random sampling in `numpy.random`](https://docs.scipy.org/doc/numpy/reference/routines.random.html)\n",
+    "* [Python slicing](https://www.pythoncentral.io/how-to-slice-listsarrays-and-tuples-in-python/)"
    ]
   }
  ],
diff --git a/getting_started/04_numpy.md b/getting_started/04_numpy.md
index 0e128dd..dce3532 100644
--- a/getting_started/04_numpy.md
+++ b/getting_started/04_numpy.md
@@ -38,6 +38,9 @@ out of date, but we will update it for the next release of FSL.
  * [Indexing multi-dimensional arrays](#indexing-multi-dimensional-arrays)
  * [Boolean indexing](#boolean-indexing)
  * [Coordinate array indexing](#coordinate-array-indexing)
+* [Exercises](#exercises)
+ * [Load an array from a file and do stuff with it](#load-an-array-from-a-file-and-do-stuff-with-it)
+ * [Concatenate affine transforms](#concatenate-affine-transforms)
 
 * [Appendix A: Generating random numbers](#appendix-generating-random-numbers)
 * [Appendix B: Importing Numpy](#appendix-importing-numpy)
@@ -853,8 +856,11 @@ print(a[evenrows, evencols])
 ## Exercises
 
 
-The challenge for each of these exercises is to complete them with as few
-lines of code as possible (whitespace and comments don't count)!
+The challenge for each of these exercises is to complete them in as few lines
+of code as possible!
+
+
+> You can find example answers to the exercises in `04_numpy/.solutions`.
 
 
 <a class="anchor" id="load-an-array-from-a-file-and-do-stuff-with-it"></a>
@@ -863,25 +869,31 @@ lines of code as possible (whitespace and comments don't count)!
 
 Load the file `04_numpy/2d_array.txt`, and calculate and print the mean for
 each column.  If your code doesn't work, you might want to __LOOK AT YOUR
-DATA__, as you will have learnt during the FSL course.
+DATA__, as you will have learnt if you have ever attended the FSL course.
 
 
 > Bonus: Find the hidden message (hint:
 > [chr](https://docs.python.org/3/library/functions.html#chr))
 
 
-
 <a class="anchor" id="concatenate-affine-transforms"></a>
 ### Concatenate affine transforms
 
 
 Given all of the files in `04_numpy/xfms/`, create a transformation matrix
 which can transform coordinates from subject 1 functional space to subject 2
-functional space<sup>4></sup>.
+functional space<sup>4</sup>.
 
+Write some code to transform the following coordinates from subject 1
+functional space to subject 2 functional space, to test that your matrix is
+correct:
 
-TODO Test coordinates
 
+| __Subject 1 coordinates__ | __Subject 2 coordinates__ |
+|:-------------------------:|:-------------------------:|
+| `[ 0.0,   0.0,   0.0]`    | `[ 3.21,   4.15, -9.89]`  |
+| `[-5.0, -20.0,  10.0]`    | `[-0.87, -14.36, -1.13]`  |
+| `[20.0,  25.0,  60.0]`    | `[29.58,  27.61, 53.37]`  |
 
 
 > <sup>4</sup> Even though these are FLIRT transforms, this is just a toy
@@ -892,8 +904,6 @@ TODO Test coordinates
 > if you actually need to work with FLIRT transforms.
 
 
-> You can find example answers to the exercises in `04_numpy/.solutions`.
-
 
 <a class="anchor" id="appendix-generating-random-numbers"></a>
 ## Appendix A: Generating random numbers
-- 
GitLab