Spatio-temporal Analysis of event-related fMRI dataΒΆ
In this example we are going to take a look at an event-related analysis of timeseries data. We will do this on fMRI data, implementing a spatio-temporal analysis of multi-volume samples. It starts as usual by loading PyMVPA and the fMRI dataset.
from mvpa2.suite import *
ds = load_tutorial_data(roi=(36,38,39,40))
The dataset we have just loaded is the full timeseries of voxels in the ventral temporal cortex for 12 concatenated experiment runs. Although originally a block-design experiment, we’ll analyze it in an event-related fashion, where each stimulation block will be considered as an individual event.
For an event-related analysis most of the processing is done on data samples that are somehow derived from a set of events. The rest of the data could be considered irrelevant. However, some preprocessing is only meaningful when performed on the full timeseries and not on the segmented event samples. An example is the detrending that typically needs to be done on the original, continuous timeseries.
In its current shape our dataset consists of 1452 samples that represent
contiguous fMRI volumes. At this stage we can easily perform linear
detrending. We are going to do it per each experiment run (the dataset has
to have runs encoded in the chunk
sample attribute), since we do not assume a
contiguous linear trend throughout the whole recording session.
# detrend on full timeseries
poly_detrend(ds, polyord=1, chunks_attr='chunks')
Let’s make a copy of the detrended dataset that we can later on use for some visualization.
orig_ds = ds.copy()
We still need to normalize each feature (i.e. a voxel at this point). In this case we are going to Z-score them, using the mean and standard deviation from the experiment’s rest condition. The resulting values might be interpreted as “activation scores”. We are again doing it per each run.
zscore(ds, chunks_attr='chunks', param_est=('targets', 'rest'))
After detrending and normalization, we can now segment the timeseries into a set of events. To achieve this we have to compile a list of event definitions first. In this example we will simply convert the block-design setup defined by the samples attributes into events, so that each stimulation block becomes an event with an associated onset and duration. The events are defined by a change in any of the provided attributes, hence we get an event for starting stimulation block and any start of a run in the experiment.
events = find_events(targets=ds.sa.targets, chunks=ds.sa.chunks)
events
is a simple list of event definitions (each one being a
dictionary) that can easily inspected for startpoints and duration of
events. Later on we want to look at the sensitivity profile ranging from
just before until a little after each block. Therefore we are slightly
moving the event onsets prior the block start and request to extract a
set of 13 consecutive volumes a as sample for each event. Finally, in this
example we are only interested in face
or house
blocks.
# filter out events
events = [ev for ev in events if ev['targets'] in ['house', 'face']]
# modify event start and set uniform duration
for ev in events:
ev['onset'] -= 2
ev['duration'] = 13
Now we get to the core of an event-related analysis. We turn our existing timeseries datasets into one with samples of timeseries segments.
PyMVPA offers eventrelated_dataset()
to perform this conversion – given a list of events and a dataset with
samples that are sorted by time. If a dataset has information about
acquisition time eventrelated_dataset()
can also convert event-definition in real time.
evds = eventrelated_dataset(ds, events=events)
Now we have our final dataset with spatio-temporal fMRI samples. Look at the attributes of the dataset to see what information is available about each event. The rest is pretty much standard.
We want to perform a cross-validation analysis of a SVM classifier. We are not primarily interested in its performance, but in the weights it assigns to the features. Remember, each feature is now voxel-timepoint, so we get a chance of looking at the spatio-temporal profile of classification relevant information in the data. We will nevertheless enable computing a confusion matrix, so we can assure ourselves that the classifier is performing reasonably well, since only a generalizing classifier model is worth inspecting, as otherwise the assigned weights are meaningless.
clf = LinearCSVMC()
sclf = SplitClassifier(clf, enable_ca=['stats'])
# Compute sensitivity, which internally trains the classifier
analyzer = sclf.get_sensitivity_analyzer()
sensitivities = analyzer(evds)
Now let’s look at the confusion matrix – it turns out that the classifier performs excellent.
print sclf.ca.stats
We could now convert the computed sensitivities back into a 4D fMRI image to look at the spatio-temporal sensitivity profile using the datasets mapper. However, in this example we are going to plot it for two example voxels and compare it to the actual signal timecourse prior and after normalization.
# example voxel coordinates
example_voxels = [(28,25,25), (28,23,25)]
First we plot the orginal signal after initial detrending. To do this, we apply the timeseries segmentation to the original detrended dataset and plot to mean signal for all face and house events for both of our example voxels.
vx_lty = ['-', '--']
t_col = ['b', 'r']
pl.subplot(311)
for i, v in enumerate(example_voxels):
slicer = np.array([tuple(idx) == v for idx in ds.fa.voxel_indices])
evds_detrend = eventrelated_dataset(orig_ds[:, slicer], events=events)
for j, t in enumerate(evds.uniquetargets):
pl.plot(np.mean(evds_detrend[evds_detrend.sa.targets == t], axis=0),
t_col[j] + vx_lty[i],
label='Voxel %i: %s' % (i, t))
pl.ylabel('Detrended signal')
pl.axhline(linestyle='--', color='0.6')
pl.legend()
In the next step we do exactly the same again, but this time for the normalized data.
pl.subplot(312)
for i, v in enumerate(example_voxels):
slicer = np.array([tuple(idx) == v for idx in ds.fa.voxel_indices])
evds_norm = eventrelated_dataset(ds[:, slicer], events=events)
for j, t in enumerate(evds.uniquetargets):
pl.plot(np.mean(evds_norm[evds_norm.sa.targets == t], axis=0),
t_col[j] + vx_lty[i])
pl.ylabel('Normalized signal')
pl.axhline(linestyle='--', color='0.6')
Finally, we plot the associated SVM weight profile for each peristimulus
timepoint of both voxels. For easier selection we do a little trick and
reverse-map the sensitivity profile through the last mapper in the
dataset’s chain mapper (look at evds.a.mapper
for the whole chain).
This will reshape the sensitivities into cross-validation fold x volume x
voxel features
.
pl.subplot(313)
smaps = evds.a.mapper[-1].reverse(sensitivities)
for i, v in enumerate(example_voxels):
slicer = np.array([tuple(idx) == v for idx in ds.fa.voxel_indices])
smap = smaps.samples[:,:,slicer].squeeze()
plot_err_line(smap, fmt='ko', linestyle=vx_lty[i])
pl.xlim((0,12))
pl.ylabel('Sensitivity')
pl.axhline(linestyle='--', color='0.6')
pl.xlabel('Peristimulus volumes')
This demo showed an event-related data analysis. Although we have performed it on fMRI data, an analogous analysis can be done for any timeseries-based data in an almost identical fashion.
See also
The full source code of this example is included in the PyMVPA source distribution (doc/examples/eventrelated.py
).