Disclaimer/Appraisal: This excercise is a based on Practical NeuroImaging course tought at Berkeley by Matthew Brett (nibabel). His copyright and distributed under Creative Common Attribution (CC-BY 2.0) license. The BOLD data files/ds114_sub009_t2r1.nii.gz
comes from the OpenFMRI project dataset ds114
: https://openfmri.org/dataset/ds000114 . It is a recompressed and renamed copy of sub009/BOLD/task002_run001/bold.nii.gz
.
# - 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
# - show figures inside the notebook
%matplotlib inline
# - import common modules
import numpy as np # the Python array package
import matplotlib.pyplot as plt # the Python plotting package
Scipy is a large library of scientific routines that builds on top of numpy.
You can think of numpy as being a subset of MATLAB, and numpy + scipy as being as being roughly equivalent to MATLAB plus the MATLAB toolboxes.
Scipy has many sub-packages, for doing things like reading MATLAB .mat
files (scipy.io
) or working with sparse matrices (scipy.sparse
). We are going to be using the functions and objects for working with statistical distributions in scipy.stats
:
import scipy.stats
scipy.stats
contains objects for working with many different distributions. We are going to be working with scipy.stats.gamma
, which implements the gamma distribution.
from scipy.stats import gamma
In particular we are interested in the probability density function (PDF) of the gamma distribution.
Because this is a function, we need to pass it an array of values at which it will evaluate.
We can also pass various parameters which change the shape, location and width of the gamma PDF. The most important is the first parameter (after the input array) known as the shape parameter ($k$ in the wikipedia page on gamma distributions).
First we chose some x values at which to sample from the gamma PDF:
x = np.arange(0, 25, 0.1)
Next we plot the gamma PDF for shape values of 2, 4, 8, 12.
plt.plot(x, gamma.pdf(x, 2), label='k=2')
plt.plot(x, gamma.pdf(x, 4), label='k=4')
plt.plot(x, gamma.pdf(x, 8), label='k=8')
plt.plot(x, gamma.pdf(x, 12), label='k=12')
plt.legend()
We will see more from the statistics distributions next week.
By "correlation coefficient", we mean the Pearson product moment correlation coefficient.
Let's say I have two arrays, x
and y
:
x = np.array([ 20.57, 18.33, 20.8 , 18.77, 18.46, 21.09, 19.96, 20.61,
18.73, 19.6 , 18.15, 19.7 , 20.36, 20.39, 19.67, 20.73])
y = np.array([ 28.98, 30.13, 29.64, 29.87, 28.78, 33.48, 31.36, 31.04,
30.43, 31.69, 27.08, 29.25, 29.5 , 30.04, 29.74, 30.06])
plt.plot(x, y, 'o')
The numpy routine np.corrcoef
gives me the correlation matrix between x
and y
:
corr_matrix = np.corrcoef(x, y)
corr_matrix
corr_matrix[0, 0]
is the correlation of x
with itself, corr_matrix[1, 0]
(and corr_matrix[0, 1]
) is the correlation of x
with y
.
Be careful - np.correlate
is something else entirely.
Question: what is it?
If I have two vectors $\mathbf{a}$ with elements $a_0, a_1, ... a_{n-1}$, and $\mathbf{b}$ with elements $b_0, b_1, ... b_{n-1}$ then the dot product is defined as:
$$ \mathbf{a}\cdot \mathbf{b} = \sum_{i=0}^{n-1} a_ib_i = a_0b_0 + a_1b_1 + \cdots + a_{n-1}b_{n-1} $$In code:
a = np.arange(5)
b = np.arange(10, 15)
np.dot(a, b)
# The same thing as
np.sum(a * b) # Elementwise multiplication
Matrix multiplication operates by taking dot products of the rows of the first array (matrix) with the columns of the second.
Let's say I have a matrix $\mathbf{X}$, and $X_{i,:}$ is row $i$ in $\mathbf{X}$. I have a matrix $\mathbf{Y}$, and $Y_{:,j}$ is column $j$ in $\mathbf{Y}$. The output matrix $\mathbf{Z} = \mathbf{X} \mathbf{Y}$ has entry $Z_{i,j} = X_{i,:} \cdot Y_{:, j}$. We will see this often over the next few weeks.
X = np.array([[0, 1, 2], [3, 4, 5]])
X
Y = np.array([[7, 8], [9, 10], [11, 12]])
Y
X.dot(Y)
X[0, :].dot(Y[:, 0])
X[1, :].dot(Y[:, 0])
Let's say I have a neural vector with a couple of spikes:
times = np.arange(0, 60, 0.5) # samples every 0.5 seconds
neural_vector = np.zeros(times.shape)
neural_vector[10] = 1 # At 5 seconds
neural_vector[20] = 1 # At 10 seconds
plt.plot(times, neural_vector)
Then I have my HRF function, sampled every half second, to match:
def hrf(t):
"A hemodynamic response function"
values = t ** 8.6 * np.exp(-t / 0.547)
# Scale max to 1
return values / np.max(values)
hrf_times = np.arange(0, 20, 0.5)
hrf_samples = hrf(hrf_times)
plt.plot(hrf_times, hrf_samples)
The input neural vector is length 120, and the HRF vector is length 40:
print(len(neural_vector))
print(len(hrf_samples))
For reasons that may be familiar to you now, when we convolve the neural vector with the hrf signal, we get an output that is length 120 + 40 - 1 = 159:
hemodynamic_prediction = np.convolve(neural_vector, hrf_samples)
len(hemodynamic_prediction)
This is because of the HRF falling off the end of the input vector. The value at index 120 in the new vector refers to time 60, and value 121 refers to time 60.5 seconds. To retain only the values in the new hemodynamic vector that refer to times up to (not including) 60s, we can just drop the last len(hrf_signal) - 1
values:
hemodynamic_for_60s = hemodynamic_prediction[:len(neural_vector)]
plt.plot(times, neural_vector, label='neural vector')
plt.plot(times, hemodynamic_for_60s, label='hemodynamic prediction')
plt.legend()
The file files/mt_hrf_estimates.txt
contains the estimated FMRI signal from voxels in the MT motion area at 0, 2, 4, ..., 28 seconds after a brief moving visual stimulus (see: http://nipy.org/nitime/examples/event_related_fmri.html).
Here are the first four rows. The numbers are in exponential floating point format:
1.394086932967517900e-01
3.938585701431884800e-01
5.012927230566770476e-01
5.676763716149294536e-01
Read the values from the file into an array mt_hrf_estimates
. Make a new array mt_hrf_times
with the times of acquisition (0, 2, ...). Plot them together to see the HRF estimate at these times:
# Load the estimated values from the text file into an array
# Make an array of corresponding times
# Plot signal by time
We want to make a hemodynamic response function that matches this shape.
Our function will accept an array that gives the times we want to calculate the HRF for, and returns the values of the HRF for those times. We will assume that the true HRF starts at zero, and gets to zero sometime before 35 seconds.
Like SPM, I'm going to try using the sum of two gamma distribution probability density functions.
# - import the gamma density function
from scipy.stats import gamma
Here's my first shot:
# - my attempt - you can do better than this
def not_great_hrf(times):
""" Return values for HRF at given times """
# Gamma pdf for the peak
peak_values = gamma.pdf(times, 6)
# Gamma pdf for the undershoot
undershoot_values = gamma.pdf(times, 12)
# Combine them
values = peak_values - 0.35 * undershoot_values
# Scale max to 0.6
return values / np.max(values) * 0.6
# - plot the data against our estimate
plt.plot(mt_hrf_times, not_great_hrf(mt_hrf_times), label='not_great_hrf')
plt.plot(mt_hrf_times, mt_hrf_estimates, label='mt_hrf_estimates')
plt.legend()
Now see if you can make a better function by playing with the Gamma distribution pdf parameter, and the mix of the two gamma distribution functions. Call your function mt_hrf
# Your "mt_hrf" function here
# Plot your function against the mt_hrf_estimates data to test
For extra points - other than looking at these plots, how would you convince me your function is better than mine?
We are going to be analyzing the data for the 4D image files/ds114_sub009_t2r1.nii.gz
.
Load the image into an image object with nibabel, and get the image data array. Print the shape.
# Load the image with nibabel, get the image data array, print the data shape
Last week we read in the stimulus file for this run to make an on - off timeseries.
The stimulus file is files/ds114_sub009_t2r1_cond.txt
.
Here's a version of the same thing we did last week, as a function:
# - read in stimulus file, return neural prediction
def events2neural(task_fname, tr, n_trs):
""" Return predicted neural time course from event file
Parameters
----------
task_fname : str
Filename of event file
tr : float
TR in seconds
n_trs : int
Number of TRs in functional run
Returns
-------
time_course : array shape (n_trs,)
Predicted neural time course, one value per TR
"""
task = np.loadtxt(task_fname)
if task.ndim != 2 or task.shape[1] != 3:
raise ValueError("Is {0} really a task file?", task_fname)
task[:, :2] = task[:, :2] / tr
time_course = np.zeros(n_trs)
for onset, duration, amplitude in task:
time_course[onset:onset + duration] = amplitude
return time_course
Use this function to read files/ds114_sub009_t2r1_cond.txt
and return a predicted neural time course.
The TR for this run is 2.5. You know the number of TRs from the image data shape above.
# Read the stimulus data file and return a predicted neural time course.
# Plot the predicted neural time course.
We discovered last week that the first volume in this run was bad. Use your slicing skills to make a new array called data_no_0
that is the data array without the first volume:
# Make new array excluding the first volume
# data_no_0 = ?
Our neural prediction time series currently has one value per volume, including the first volume. To match the data,
make a new neural prediction variable that does not include the first value of the time series. Call this new variable neural_prediction_no_0
.
# Knock the first element off the neural prediction time series
# neural_prediction_no_0 = ?
For now, we're going to play with data for a single voxel.
Last week, we subtracted the rest from the task scans, something like this:
# - subtracting rest from task scans
task_scans = data_no_0[..., neural_prediction_no_0 == 1]
rest_scans = data_no_0[..., neural_prediction_no_0 == 0]
difference = np.mean(task_scans, axis=-1) - np.mean(rest_scans, axis=-1)
# - showing slice 14 from the difference image
plt.imshow(difference[:, :, 14], cmap='gray')
It looks like there's a voxel that is greater for activation than rest at about (i, j, k) == (45, 43, 14).
Get and plot the values for this voxel position, for every volume in the 4D data (not including the first volume). You can do it with a loop, but slicing is much nicer.
# Get the values for (i, j, k) = (45, 43, 14) and every volume
# Plot the values (voxel time course)
Correlate the predicted neural time series with the voxel time course:
# Correlation of predicted neural time course with voxel signal time course
Now we will do a predicted hemodynamic time course using convolution.
Next we need to get the HRF vector to convolve with.
Remember we have defined the HRF as a function of time, not TRs.
For our convolution, we need to sample the HRF at times corresponding the start of the TRs.
So, we need to sample at times (0, 2.5, ...)
Make a vector of times at which to sample the HRF. We want to sample every TR up until (but not including) somewhere near 35 seconds (where the HRF should have got close to zero again).
# Make a vector of times at which to sample the HRF
Sample your HRF function at these times and plot:
# Sample HRF at given times
# Plot HRF samples against times
Convolve the predicted neural time course with the HRF samples:
# Convolve predicted neural time course with HRF samples
The default output of convolve is longer than the input neural prediction vector, by the length of the convolving vector (the HRF samples) minus 1. Knock these last values off the result of the convolution to get a vector the same length as the neural prediction:
# Remove extra tail of values put there by the convolution
Plot the convolved neural prediction, and then, on the same plot, plot the unconvolved neural prediction.
# Plot convolved neural prediction and unconvolved neural prediction
Does the new convolved time course correlate better with the voxel time course?
# Correlation of the convolved time course with voxel time course
Plot the hemodynamic prediction against the actual signal (voxel values). Remember to use a marker such as '+' to give you a scatter plot. How does it look?