From b8e2fb7e87347e5ac84b93154754099d95708de4 Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Wed, 14 Feb 2018 11:06:49 -0500 Subject: [PATCH 001/210] Move get_clusters_table from thresholding to reporting. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Don’t report peaks in matrix space. - Report cluster size in mm3 instead of voxels. - Report top 3 subpeaks more than 8mm separated when available. - Add argument for voxel-connectivity for cluster definition. --- nistats/reporting.py | 130 ++++++++++++++++++++++++++++++++++++++++ nistats/thresholding.py | 83 ------------------------- 2 files changed, 130 insertions(+), 83 deletions(-) diff --git a/nistats/reporting.py b/nistats/reporting.py index fd990d19..b23a0c4a 100644 --- a/nistats/reporting.py +++ b/nistats/reporting.py @@ -6,16 +6,146 @@ import os import warnings +from string import ascii_lowercase + import numpy as np +import pandas as pd from scipy import stats import nilearn.plotting # overrides the backend on headless servers +from nilearn.image.resampling import coord_transform import matplotlib import matplotlib.pyplot as plt +import scipy.ndimage.measurements as meas +from skimage.feature import peak_local_max from patsy import DesignInfo + from .design_matrix import check_design_matrix matplotlib.rc('xtick', labelsize=20) +def _get_conn(conn='corners'): + if conn == 'corners': + return np.ones((3, 3, 3), int) + elif conn == 'edges': + mat = np.ones((3, 3, 3), int) # 18 connectivity + for i in [0, -1]: + for j in [0, -1]: + for k in [0, -1]: + mat[i, j, k] = 0 + return mat + elif conn == 'faces': + mat = np.zeros((3, 3, 3), int) + mat[1, 1, :] = 1 + mat[1, :, 1] = 1 + mat[:, 1, 1] = 1 + return mat + else: + raise Exception('Connectivity pattern "{0}" unknown.'.format(conn)) + + +def get_clusters_table(stat_img, stat_threshold, cluster_threshold=None, + connectivity='corners'): + """Creates pandas dataframe with img cluster statistics. + + Parameters + ---------- + stat_img : Niimg-like object, + statistical image (presumably in z scale) + + stat_threshold: float, optional + cluster forming threshold (either a p-value or z-scale value) + + cluster_threshold : int, optional + cluster size threshold + + Returns + ------- + Pandas dataframe with img clusters + """ + cols = ['Cluster ID', 'X', 'Y', 'Z', 'Peak Stat', 'Cluster Size (mm3)'] + stat_map = stat_img.get_data() + conn_mat = _get_conn(connectivity) + voxel_size = np.prod(stat_img.get_header().get_zooms()) + + # Binarize + binarized = stat_map > stat_threshold + binarized = binarized.astype(int) + + # If the stat threshold is too high simply return an empty dataframe + if np.sum(binarized) == 0: + warnings.warn('Attention: No clusters with stat higher than %f' % + stat_threshold) + return pd.DataFrame(columns=cols) + + # Extract connected components above cluster size threshold + label_map = meas.label(binarized, conn_mat)[0] + clust_ids = sorted(list(np.unique(label_map)[1:])) + for c_val in clust_ids: + if cluster_threshold is not None and np.sum(label_map == c_val) < cluster_threshold: + stat_map[label_map == c_val] = 0 + binarized[label_map == c_val] = 0 + + # If the cluster threshold is too high simply return an empty dataframe + # this checks for stats higher than threshold after small clusters + # were removed from stat_map + if np.sum(stat_map > stat_threshold) == 0: + warnings.warn('Attention: No clusters with more than %d voxels' % + cluster_threshold) + return pd.DataFrame(columns=cols) + + # Now re-label and create table + label_map = meas.label(binarized, conn_mat)[0] + clust_ids = sorted(list(np.unique(label_map)[1:])) + peak_vals = np.array([np.max(stat_map * (label_map == c)) for c in clust_ids]) + clust_order = (-peak_vals).argsort() # Sort by descending max value + clust_ids = [clust_ids[c] for c in clust_order] + + rows = [] + for c_id, c_val in enumerate(clust_ids): + cluster_mask = label_map == c_val + masked_data = stat_map * cluster_mask + + # k + cluster_size_vox = np.sum(cluster_mask) + cluster_size_mm = int(cluster_size_vox * voxel_size) + + # xyz and val + def _get_val(row, input_arr): + """Small function for extracting values from array based on index. + """ + i, j, k = row + return input_arr[i, j, k] + + subpeak_dist = int(np.round(8. / np.cbrt(voxel_size))) # 8mm dist + subpeak_ijk = peak_local_max(masked_data, min_distance=subpeak_dist, num_peaks=4) + subpeak_vals = np.apply_along_axis(arr=subpeak_ijk, axis=1, + func1d=_get_val, + input_arr=masked_data) + order = (-subpeak_vals).argsort() + subpeak_ijk = subpeak_ijk[order, :] + subpeak_xyz = np.asarray( + coord_transform( + subpeak_ijk[:, 0], subpeak_ijk[:, 1], subpeak_ijk[:, 2], + stat_img.affine)).tolist() + subpeak_xyz = np.array(subpeak_xyz).T + subpeak_vals = subpeak_vals[order] + + for subpeak in range(len(subpeak_vals)): + if subpeak == 0: + row = [c_id+1, + subpeak_xyz[subpeak, 0], subpeak_xyz[subpeak, 1], subpeak_xyz[subpeak, 2], + subpeak_vals[subpeak], cluster_size_mm] + else: + sp_id = '{0}{1}'.format(c_id+1, ascii_lowercase[subpeak-1]) + row = [sp_id, + subpeak_xyz[subpeak, 0], subpeak_xyz[subpeak, 1], subpeak_xyz[subpeak, 2], + subpeak_vals[subpeak], ''] + rows += [row] + df = pd.DataFrame(columns=cols, data=rows) + df.set_index('Cluster ID', inplace=True) + return df + + def compare_niimgs(ref_imgs, src_imgs, masker, plot_hist=True, log=True, ref_label="image set 1", src_label="image set 2", output_dir=None, axes=None): diff --git a/nistats/thresholding.py b/nistats/thresholding.py index 4bdae008..3cbc97f3 100644 --- a/nistats/thresholding.py +++ b/nistats/thresholding.py @@ -7,9 +7,6 @@ from scipy.ndimage import label from scipy.stats import norm from nilearn.input_data import NiftiMasker -from nilearn.image.resampling import coord_transform -import pandas as pd -from warnings import warn def fdr_threshold(z_vals, alpha): @@ -86,83 +83,3 @@ def map_threshold(stat_img, mask_img=None, threshold=.001, stats[labels == label_] = 0 return masker.inverse_transform(stats), z_th - - -def get_clusters_table(stat_img, stat_threshold, cluster_threshold): - """Creates pandas dataframe with img cluster statistics. - - Parameters - ---------- - stat_img : Niimg-like object, - statistical image (presumably in z scale) - - stat_threshold: float, optional - cluster forming threshold (either a p-value or z-scale value) - - cluster_threshold : int, optional - cluster size threshold - - Returns - ------- - Pandas dataframe with img clusters - """ - - stat_map = stat_img.get_data() - - # If the stat threshold is too high simply return an empty dataframe - if np.sum(stat_map > stat_threshold) == 0: - warn('Attention: No clusters with stat higher than %f' % - stat_threshold) - return pd.DataFrame() - - # Extract connected components above threshold - label_map, n_labels = label(stat_map > stat_threshold) - - for label_ in range(1, n_labels + 1): - if np.sum(label_map == label_) < cluster_threshold: - stat_map[label_map == label_] = 0 - - # If the cluster threshold is too high simply return an empty dataframe - # this checks for stats higher than threshold after small clusters - # were removed from stat_map - if np.sum(stat_map > stat_threshold) == 0: - warn('Attention: No clusters with more than %d voxels' % - cluster_threshold) - return pd.DataFrame() - - label_map, n_labels = label(stat_map > stat_threshold) - label_map = np.ravel(label_map) - stat_map = np.ravel(stat_map) - - peaks = [] - max_stat = [] - clusters_size = [] - coords = [] - for label_ in range(1, n_labels + 1): - cluster = stat_map.copy() - cluster[label_map != label_] = 0 - - peak = np.unravel_index(np.argmax(cluster), - stat_img.get_data().shape) - peaks.append(peak) - - max_stat.append(np.max(cluster)) - - clusters_size.append(np.sum(label_map == label_)) - - x_map, y_map, z_map = peak - mni_coords = np.asarray( - coord_transform( - x_map, y_map, z_map, stat_img.get_affine())).tolist() - mni_coords = [round(x) for x in mni_coords] - coords.append(mni_coords) - - vx, vy, vz = zip(*peaks) - x, y, z = zip(*coords) - - columns = ['Vx', 'Vy', 'Vz', 'X', 'Y', 'Z', 'Peak stat', 'Cluster size'] - clusters_table = pd.DataFrame( - list(zip(vx, vy, vz, x, y, z, max_stat, clusters_size)), - columns=columns) - - return clusters_table From 3a0c9ba918a91c577fd8a471e45b04d5f14c550c Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Thu, 15 Feb 2018 08:59:09 -0500 Subject: [PATCH 002/210] Add skimage to requirements. Also, I removed the connectivity argument for get_clusters_table. I kept the separate neighborhood matrix function (`_get_conn`), just in case. Currently, it uses 6-connectivity (aka NN1, faces-only connectivity, or first-nearest neighbor clustering). --- nistats/reporting.py | 90 ++++++++++++++++++++++++-------------------- nistats/version.py | 4 ++ 2 files changed, 54 insertions(+), 40 deletions(-) diff --git a/nistats/reporting.py b/nistats/reporting.py index b23a0c4a..6726b535 100644 --- a/nistats/reporting.py +++ b/nistats/reporting.py @@ -23,51 +23,68 @@ matplotlib.rc('xtick', labelsize=20) -def _get_conn(conn='corners'): - if conn == 'corners': - return np.ones((3, 3, 3), int) - elif conn == 'edges': - mat = np.ones((3, 3, 3), int) # 18 connectivity - for i in [0, -1]: - for j in [0, -1]: - for k in [0, -1]: - mat[i, j, k] = 0 - return mat - elif conn == 'faces': +def _get_conn(neighborhood=6): + """Generate 3x3x3 connectivity matrix for cluster labeling based on voxel + neighborhood definition. + + Parameters + ---------- + neighborhood : {6, 18, 26} + Voxel connectivity level. + 6: Voxels must be connected by faces. + 18: Voxels may be connected by faces or by edges. + 26: Voxels may be connected by faces, edges, or corners. + + Returns + ------- + mat : :obj:`numpy.ndarray` + 3x3x3 array of 1s and 0s. A 1 indicates that that a voxel located in + that position relative to a voxel located in the center of the array + would be considered part of the same cluster as the central voxel. + """ + if neighborhood == 6: mat = np.zeros((3, 3, 3), int) mat[1, 1, :] = 1 mat[1, :, 1] = 1 mat[:, 1, 1] = 1 - return mat + elif neighborhood == 18: + mat = np.zeros((3, 3, 3), int) + mat[:, :, 1] = 1 + mat[:, 1, :] = 1 + mat[1, :, :] = 1 + elif neighborhood == 26: + mat = np.ones((3, 3, 3), int) else: - raise Exception('Connectivity pattern "{0}" unknown.'.format(conn)) + raise Exception('Neighborhood must be `int` in set (6, 18, 26).') + return mat -def get_clusters_table(stat_img, stat_threshold, cluster_threshold=None, - connectivity='corners'): +def get_clusters_table(stat_img, stat_threshold, cluster_threshold=None): """Creates pandas dataframe with img cluster statistics. Parameters ---------- stat_img : Niimg-like object, - statistical image (presumably in z scale) + Statistical image (presumably in z- or p-scale). - stat_threshold: float, optional - cluster forming threshold (either a p-value or z-scale value) + stat_threshold: :obj:`float` + Cluster forming threshold in same scale as `stat_img` (either a + p-value or z-scale value). - cluster_threshold : int, optional - cluster size threshold + cluster_threshold : :obj:`int` or :obj:`None`, optional + Cluster size threshold, in voxels. Returns ------- - Pandas dataframe with img clusters + df : :obj:`pandas.DataFrame` + Table with peaks and subpeaks from thresholded `stat_img`. """ cols = ['Cluster ID', 'X', 'Y', 'Z', 'Peak Stat', 'Cluster Size (mm3)'] stat_map = stat_img.get_data() - conn_mat = _get_conn(connectivity) - voxel_size = np.prod(stat_img.get_header().get_zooms()) + conn_mat = _get_conn(6) # 6-connectivity, aka NN1 or "faces" + voxel_size = np.prod(stat_img.header.get_zooms()) - # Binarize + # Binarize using CDT binarized = stat_map > stat_threshold binarized = binarized.astype(int) @@ -97,17 +114,14 @@ def get_clusters_table(stat_img, stat_threshold, cluster_threshold=None, label_map = meas.label(binarized, conn_mat)[0] clust_ids = sorted(list(np.unique(label_map)[1:])) peak_vals = np.array([np.max(stat_map * (label_map == c)) for c in clust_ids]) - clust_order = (-peak_vals).argsort() # Sort by descending max value - clust_ids = [clust_ids[c] for c in clust_order] + clust_ids = [clust_ids[c] for c in (-peak_vals).argsort()] # Sort by descending max value rows = [] for c_id, c_val in enumerate(clust_ids): cluster_mask = label_map == c_val masked_data = stat_map * cluster_mask - # k - cluster_size_vox = np.sum(cluster_mask) - cluster_size_mm = int(cluster_size_vox * voxel_size) + cluster_size_mm = int(np.sum(cluster_mask) * voxel_size) # xyz and val def _get_val(row, input_arr): @@ -116,33 +130,29 @@ def _get_val(row, input_arr): i, j, k = row return input_arr[i, j, k] - subpeak_dist = int(np.round(8. / np.cbrt(voxel_size))) # 8mm dist + subpeak_dist = int(np.round(8. / np.cbrt(voxel_size))) # pylint: disable=no-member subpeak_ijk = peak_local_max(masked_data, min_distance=subpeak_dist, num_peaks=4) - subpeak_vals = np.apply_along_axis(arr=subpeak_ijk, axis=1, - func1d=_get_val, + subpeak_vals = np.apply_along_axis(arr=subpeak_ijk, axis=1, func1d=_get_val, input_arr=masked_data) order = (-subpeak_vals).argsort() + subpeak_vals = subpeak_vals[order] subpeak_ijk = subpeak_ijk[order, :] subpeak_xyz = np.asarray( coord_transform( subpeak_ijk[:, 0], subpeak_ijk[:, 1], subpeak_ijk[:, 2], stat_img.affine)).tolist() subpeak_xyz = np.array(subpeak_xyz).T - subpeak_vals = subpeak_vals[order] for subpeak in range(len(subpeak_vals)): if subpeak == 0: - row = [c_id+1, - subpeak_xyz[subpeak, 0], subpeak_xyz[subpeak, 1], subpeak_xyz[subpeak, 2], - subpeak_vals[subpeak], cluster_size_mm] + row = [c_id+1, subpeak_xyz[subpeak, 0], subpeak_xyz[subpeak, 1], + subpeak_xyz[subpeak, 2], subpeak_vals[subpeak], cluster_size_mm] else: sp_id = '{0}{1}'.format(c_id+1, ascii_lowercase[subpeak-1]) - row = [sp_id, - subpeak_xyz[subpeak, 0], subpeak_xyz[subpeak, 1], subpeak_xyz[subpeak, 2], - subpeak_vals[subpeak], ''] + row = [sp_id, subpeak_xyz[subpeak, 0], subpeak_xyz[subpeak, 1], + subpeak_xyz[subpeak, 2], subpeak_vals[subpeak], ''] rows += [row] df = pd.DataFrame(columns=cols, data=rows) - df.set_index('Cluster ID', inplace=True) return df diff --git a/nistats/version.py b/nistats/version.py index d761e0d9..d9b208a5 100644 --- a/nistats/version.py +++ b/nistats/version.py @@ -56,6 +56,10 @@ 'min_version': '0.15.0', 'required_at_installation': True, 'install_info': _NISTATS_INSTALL_MSG}), + ('skimage', { + 'min_version': '0.13.0', + 'required_at_installation': True, + 'install_info': _NISTATS_INSTALL_MSG}), ) OPTIONAL_MATPLOTLIB_MIN_VERSION = '1.3.1' From 7f5b1e61b1e3741f7ac0c023795993632e2ab7b3 Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Thu, 15 Feb 2018 10:50:47 -0500 Subject: [PATCH 003/210] Move get_clusters_table tests from thresholding to reporting. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also, I’ve made the subpeak detection explicitly work around binary clusters. Now it just reports the center of mass. --- nistats/reporting.py | 14 ++++++++++++-- nistats/tests/test_reporting.py | 23 ++++++++++++++++++++++- nistats/tests/test_thresholding.py | 22 +--------------------- 3 files changed, 35 insertions(+), 24 deletions(-) diff --git a/nistats/reporting.py b/nistats/reporting.py index 6726b535..40227db8 100644 --- a/nistats/reporting.py +++ b/nistats/reporting.py @@ -130,8 +130,18 @@ def _get_val(row, input_arr): i, j, k = row return input_arr[i, j, k] - subpeak_dist = int(np.round(8. / np.cbrt(voxel_size))) # pylint: disable=no-member - subpeak_ijk = peak_local_max(masked_data, min_distance=subpeak_dist, num_peaks=4) + if np.std(stat_map[cluster_mask]) == 0 and cluster_size_mm > 1: + warnings.warn('Cluster appears to be single-valued. ' + 'Reporting center of mass.') + subpeak_ijk = np.round(meas.center_of_mass(masked_data, cluster_mask, + index=1)).astype(int) + subpeak_ijk = subpeak_ijk[None, :] + else: + subpeak_dist = int(np.round(8. / np.cbrt(voxel_size))) # pylint: disable=no-member + subpeak_ijk = peak_local_max(masked_data, min_distance=subpeak_dist, + threshold_abs=stat_threshold, num_peaks=4, + exclude_border=False) + subpeak_vals = np.apply_along_axis(arr=subpeak_ijk, axis=1, func1d=_get_val, input_arr=masked_data) order = (-subpeak_vals).argsort() diff --git a/nistats/tests/test_reporting.py b/nistats/tests/test_reporting.py index 3bf7c421..f7e3f182 100644 --- a/nistats/tests/test_reporting.py +++ b/nistats/tests/test_reporting.py @@ -1,7 +1,9 @@ from nistats.design_matrix import make_design_matrix -from nistats.reporting import plot_design_matrix +from nistats.reporting import plot_design_matrix, get_clusters_table +import nibabel as nib import numpy as np from numpy.testing import dec +from nose.tools import assert_true # Set the backend to avoid having DISPLAY problems from nilearn.plotting import _set_mpl_backend @@ -25,3 +27,22 @@ def test_show_design_matrix(): frame_times, drift_model='polynomial', drift_order=3) ax = plot_design_matrix(DM) assert (ax is not None) + + +def test_get_clusters_table(): + shape = (9, 10, 11) + data = np.zeros(shape) + data[2:4, 5:7, 6:8] = 5. + stat_img = nib.Nifti1Image(data, np.eye(4)) + + # test one cluster extracted + cluster_table = get_clusters_table(stat_img, 4, 0) + assert_true(len(cluster_table) == 1) + + # test empty table on high stat threshold + cluster_table = get_clusters_table(stat_img, 6, 0) + assert_true(len(cluster_table) == 0) + + # test empty table on high cluster threshold + cluster_table = get_clusters_table(stat_img, 4, 9) + assert_true(len(cluster_table) == 0) diff --git a/nistats/tests/test_thresholding.py b/nistats/tests/test_thresholding.py index 2a76945c..08e23a65 100644 --- a/nistats/tests/test_thresholding.py +++ b/nistats/tests/test_thresholding.py @@ -5,8 +5,7 @@ from nose.tools import assert_true from numpy.testing import assert_almost_equal, assert_equal import nibabel as nib -from nistats.thresholding import (fdr_threshold, map_threshold, - get_clusters_table) +from nistats.thresholding import fdr_threshold, map_threshold def test_fdr(): @@ -71,22 +70,3 @@ def test_map_threshold(): cluster_threshold=0) vals = th_map.get_data() assert_equal(np.sum(vals > 0), 8) - - -def test_get_clusters_table(): - shape = (9, 10, 11) - data = np.zeros(shape) - data[2:4, 5:7, 6:8] = 5. - stat_img = nib.Nifti1Image(data, np.eye(4)) - - # test one cluster extracted - cluster_table = get_clusters_table(stat_img, 4, 0) - assert_true(len(cluster_table) == 1) - - # test empty table on high stat threshold - cluster_table = get_clusters_table(stat_img, 6, 0) - assert_true(len(cluster_table) == 0) - - # test empty table on high cluster threshold - cluster_table = get_clusters_table(stat_img, 4, 9) - assert_true(len(cluster_table) == 0) From 8062192716ae41eda7b333aa59d2bb578db2530e Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Thu, 15 Feb 2018 10:53:19 -0500 Subject: [PATCH 004/210] Fix get_clusters_table documentation and improve warnings. --- nistats/reporting.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/nistats/reporting.py b/nistats/reporting.py index 40227db8..4a566fb4 100644 --- a/nistats/reporting.py +++ b/nistats/reporting.py @@ -77,7 +77,9 @@ def get_clusters_table(stat_img, stat_threshold, cluster_threshold=None): Returns ------- df : :obj:`pandas.DataFrame` - Table with peaks and subpeaks from thresholded `stat_img`. + Table with peaks and subpeaks from thresholded `stat_img`. For binary + clusters (clusters with >1 voxel containing only one value), the table + reports the center of mass of the cluster, rather than any peaks/subpeaks. """ cols = ['Cluster ID', 'X', 'Y', 'Z', 'Peak Stat', 'Cluster Size (mm3)'] stat_map = stat_img.get_data() @@ -131,8 +133,8 @@ def _get_val(row, input_arr): return input_arr[i, j, k] if np.std(stat_map[cluster_mask]) == 0 and cluster_size_mm > 1: - warnings.warn('Cluster appears to be single-valued. ' - 'Reporting center of mass.') + warnings.warn('Cluster {0} appears to be single-valued. ' + 'Reporting center of mass.'.format(c_id)) subpeak_ijk = np.round(meas.center_of_mass(masked_data, cluster_mask, index=1)).astype(int) subpeak_ijk = subpeak_ijk[None, :] From 14b00a877663b6c88aecf7d813dd3ecc0bfdb3e9 Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Fri, 23 Feb 2018 10:21:50 -0500 Subject: [PATCH 005/210] Switch from skimage to scipy for subpeak identification. --- nistats/reporting.py | 73 +++++++++++++++++++++++++++++--------------- 1 file changed, 49 insertions(+), 24 deletions(-) diff --git a/nistats/reporting.py b/nistats/reporting.py index 4a566fb4..7f510ee5 100644 --- a/nistats/reporting.py +++ b/nistats/reporting.py @@ -15,25 +15,55 @@ from nilearn.image.resampling import coord_transform import matplotlib import matplotlib.pyplot as plt -import scipy.ndimage.measurements as meas -from skimage.feature import peak_local_max +from scipy import ndimage from patsy import DesignInfo from .design_matrix import check_design_matrix matplotlib.rc('xtick', labelsize=20) +def _local_max(data, min_distance): + """Find all local maxima of the array, separated by at least min_distance. + From https://stackoverflow.com/a/22631583/2589328 + + Parameters + ---------- + data : array_like + 3D array of with masked values for cluster. + + min_distance : :obj:`int` + Minimum distance between local maxima in ``data``, in terms of + voxels (not mm). + + Returns + ------- + xy : :obj:`numpy.ndarray` + (n_foci, 3) array of local maxima indices for cluster. + """ + data_max = ndimage.filters.maximum_filter(data, min_distance) + maxima = (data == data_max) + data_min = ndimage.filters.minimum_filter(data, min_distance) + diff = ((data_max - data_min) > 0) + maxima[diff == 0] = 0 + + labeled, num_objects = ndimage.label(maxima) + xy = np.array(ndimage.center_of_mass(data, labeled, range(1, num_objects+1))) + xy = np.round(xy).astype(int) + return xy + + def _get_conn(neighborhood=6): """Generate 3x3x3 connectivity matrix for cluster labeling based on voxel neighborhood definition. Parameters ---------- - neighborhood : {6, 18, 26} + neighborhood : {6, 18, 26}, optional Voxel connectivity level. 6: Voxels must be connected by faces. 18: Voxels may be connected by faces or by edges. 26: Voxels may be connected by faces, edges, or corners. + Default is 6. Returns ------- @@ -59,6 +89,13 @@ def _get_conn(neighborhood=6): return mat +def _get_val(row, input_arr): + """Small function for extracting values from array based on index. + """ + i, j, k = row + return input_arr[i, j, k] + + def get_clusters_table(stat_img, stat_threshold, cluster_threshold=None): """Creates pandas dataframe with img cluster statistics. @@ -97,7 +134,7 @@ def get_clusters_table(stat_img, stat_threshold, cluster_threshold=None): return pd.DataFrame(columns=cols) # Extract connected components above cluster size threshold - label_map = meas.label(binarized, conn_mat)[0] + label_map = ndimage.measurements.label(binarized, conn_mat)[0] clust_ids = sorted(list(np.unique(label_map)[1:])) for c_val in clust_ids: if cluster_threshold is not None and np.sum(label_map == c_val) < cluster_threshold: @@ -113,7 +150,7 @@ def get_clusters_table(stat_img, stat_threshold, cluster_threshold=None): return pd.DataFrame(columns=cols) # Now re-label and create table - label_map = meas.label(binarized, conn_mat)[0] + label_map = ndimage.measurements.label(binarized, conn_mat)[0] clust_ids = sorted(list(np.unique(label_map)[1:])) peak_vals = np.array([np.max(stat_map * (label_map == c)) for c in clust_ids]) clust_ids = [clust_ids[c] for c in (-peak_vals).argsort()] # Sort by descending max value @@ -125,24 +162,9 @@ def get_clusters_table(stat_img, stat_threshold, cluster_threshold=None): cluster_size_mm = int(np.sum(cluster_mask) * voxel_size) - # xyz and val - def _get_val(row, input_arr): - """Small function for extracting values from array based on index. - """ - i, j, k = row - return input_arr[i, j, k] - - if np.std(stat_map[cluster_mask]) == 0 and cluster_size_mm > 1: - warnings.warn('Cluster {0} appears to be single-valued. ' - 'Reporting center of mass.'.format(c_id)) - subpeak_ijk = np.round(meas.center_of_mass(masked_data, cluster_mask, - index=1)).astype(int) - subpeak_ijk = subpeak_ijk[None, :] - else: - subpeak_dist = int(np.round(8. / np.cbrt(voxel_size))) # pylint: disable=no-member - subpeak_ijk = peak_local_max(masked_data, min_distance=subpeak_dist, - threshold_abs=stat_threshold, num_peaks=4, - exclude_border=False) + # Get peaks, subpeaks and associated statistics + subpeak_dist = int(np.round(8. / np.cbrt(voxel_size))) # pylint: disable=no-member + subpeak_ijk = _local_max(masked_data, min_distance=subpeak_dist) subpeak_vals = np.apply_along_axis(arr=subpeak_ijk, axis=1, func1d=_get_val, input_arr=masked_data) @@ -155,11 +177,14 @@ def _get_val(row, input_arr): stat_img.affine)).tolist() subpeak_xyz = np.array(subpeak_xyz).T - for subpeak in range(len(subpeak_vals)): + # Only report peak and, at most, top 3 subpeaks. + n_subpeaks = np.min((len(subpeak_vals), 4)) + for subpeak in range(n_subpeaks): if subpeak == 0: row = [c_id+1, subpeak_xyz[subpeak, 0], subpeak_xyz[subpeak, 1], subpeak_xyz[subpeak, 2], subpeak_vals[subpeak], cluster_size_mm] else: + # Subpeak naming convention is cluster num + letter (1a, 1b, etc.) sp_id = '{0}{1}'.format(c_id+1, ascii_lowercase[subpeak-1]) row = [sp_id, subpeak_xyz[subpeak, 0], subpeak_xyz[subpeak, 1], subpeak_xyz[subpeak, 2], subpeak_vals[subpeak], ''] From 9523da07a43266d54e8626698b8f1ae3114769cf Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Fri, 23 Feb 2018 10:41:27 -0500 Subject: [PATCH 006/210] Remove skimage dependency. --- nistats/version.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/nistats/version.py b/nistats/version.py index d9b208a5..d761e0d9 100644 --- a/nistats/version.py +++ b/nistats/version.py @@ -56,10 +56,6 @@ 'min_version': '0.15.0', 'required_at_installation': True, 'install_info': _NISTATS_INSTALL_MSG}), - ('skimage', { - 'min_version': '0.13.0', - 'required_at_installation': True, - 'install_info': _NISTATS_INSTALL_MSG}), ) OPTIONAL_MATPLOTLIB_MIN_VERSION = '1.3.1' From f0826183b159d7e40c07193c5191572040e24003 Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Fri, 23 Feb 2018 10:58:44 -0500 Subject: [PATCH 007/210] Switch np.cbrt(n) with np.power(n, 1/3.) for Travis CI. Numpy did not have `cbrt` function in 1.8.2, the version used for Python2.7 CI. --- nistats/reporting.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nistats/reporting.py b/nistats/reporting.py index 7f510ee5..c9de49b0 100644 --- a/nistats/reporting.py +++ b/nistats/reporting.py @@ -30,7 +30,7 @@ def _local_max(data, min_distance): ---------- data : array_like 3D array of with masked values for cluster. - + min_distance : :obj:`int` Minimum distance between local maxima in ``data``, in terms of voxels (not mm). @@ -163,7 +163,7 @@ def get_clusters_table(stat_img, stat_threshold, cluster_threshold=None): cluster_size_mm = int(np.sum(cluster_mask) * voxel_size) # Get peaks, subpeaks and associated statistics - subpeak_dist = int(np.round(8. / np.cbrt(voxel_size))) # pylint: disable=no-member + subpeak_dist = int(np.round(8. / np.power(voxel_size, 1./3.))) # pylint: disable=no-member subpeak_ijk = _local_max(masked_data, min_distance=subpeak_dist) subpeak_vals = np.apply_along_axis(arr=subpeak_ijk, axis=1, func1d=_get_val, From 3b343177daeff6979b50fa823e1a5242f710800f Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Mon, 26 Feb 2018 07:14:03 -0500 Subject: [PATCH 008/210] Remove _get_conn and hardcode in 6-connectivity. --- nistats/reporting.py | 42 ++++-------------------------------------- 1 file changed, 4 insertions(+), 38 deletions(-) diff --git a/nistats/reporting.py b/nistats/reporting.py index c9de49b0..dabb006d 100644 --- a/nistats/reporting.py +++ b/nistats/reporting.py @@ -52,43 +52,6 @@ def _local_max(data, min_distance): return xy -def _get_conn(neighborhood=6): - """Generate 3x3x3 connectivity matrix for cluster labeling based on voxel - neighborhood definition. - - Parameters - ---------- - neighborhood : {6, 18, 26}, optional - Voxel connectivity level. - 6: Voxels must be connected by faces. - 18: Voxels may be connected by faces or by edges. - 26: Voxels may be connected by faces, edges, or corners. - Default is 6. - - Returns - ------- - mat : :obj:`numpy.ndarray` - 3x3x3 array of 1s and 0s. A 1 indicates that that a voxel located in - that position relative to a voxel located in the center of the array - would be considered part of the same cluster as the central voxel. - """ - if neighborhood == 6: - mat = np.zeros((3, 3, 3), int) - mat[1, 1, :] = 1 - mat[1, :, 1] = 1 - mat[:, 1, 1] = 1 - elif neighborhood == 18: - mat = np.zeros((3, 3, 3), int) - mat[:, :, 1] = 1 - mat[:, 1, :] = 1 - mat[1, :, :] = 1 - elif neighborhood == 26: - mat = np.ones((3, 3, 3), int) - else: - raise Exception('Neighborhood must be `int` in set (6, 18, 26).') - return mat - - def _get_val(row, input_arr): """Small function for extracting values from array based on index. """ @@ -120,7 +83,10 @@ def get_clusters_table(stat_img, stat_threshold, cluster_threshold=None): """ cols = ['Cluster ID', 'X', 'Y', 'Z', 'Peak Stat', 'Cluster Size (mm3)'] stat_map = stat_img.get_data() - conn_mat = _get_conn(6) # 6-connectivity, aka NN1 or "faces" + conn_mat = np.zeros((3, 3, 3), int) # 6-connectivity, aka NN1 or "faces" + conn_mat[1, 1, :] = 1 + conn_mat[1, :, 1] = 1 + conn_mat[:, 1, 1] = 1 voxel_size = np.prod(stat_img.header.get_zooms()) # Binarize using CDT From b23de8824575f2ebb51f2b1e9911c7f69b9e1481 Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Mon, 26 Feb 2018 08:23:19 -0500 Subject: [PATCH 009/210] Improve _local_max and add test. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _local_max now checks distance between subpeaks explicitly, since the approach used in the StackOverflow post wasn’t quite doing it. --- nistats/reporting.py | 50 ++++++++++++++++++++------------- nistats/tests/test_reporting.py | 19 ++++++++++++- 2 files changed, 49 insertions(+), 20 deletions(-) diff --git a/nistats/reporting.py b/nistats/reporting.py index dabb006d..873ff9a0 100644 --- a/nistats/reporting.py +++ b/nistats/reporting.py @@ -24,7 +24,7 @@ def _local_max(data, min_distance): """Find all local maxima of the array, separated by at least min_distance. - From https://stackoverflow.com/a/22631583/2589328 + Adapted from https://stackoverflow.com/a/22631583/2589328 Parameters ---------- @@ -37,8 +37,11 @@ def _local_max(data, min_distance): Returns ------- - xy : :obj:`numpy.ndarray` + ijk : :obj:`numpy.ndarray` (n_foci, 3) array of local maxima indices for cluster. + + vals : :obj:`numpy.ndarray` + (n_foci,) array of values from data at ijk. """ data_max = ndimage.filters.maximum_filter(data, min_distance) maxima = (data == data_max) @@ -47,9 +50,24 @@ def _local_max(data, min_distance): maxima[diff == 0] = 0 labeled, num_objects = ndimage.label(maxima) - xy = np.array(ndimage.center_of_mass(data, labeled, range(1, num_objects+1))) - xy = np.round(xy).astype(int) - return xy + ijk = np.array(ndimage.center_of_mass(data, labeled, range(1, num_objects+1))) + ijk = np.round(ijk).astype(int) + + vals = np.apply_along_axis(arr=ijk, axis=1, func1d=_get_val, input_arr=data) + order = (-vals).argsort() + vals = vals[order] + ijk = ijk[order, :] + + # Reduce list of subpeaks based on distance + keep_idx = np.ones(ijk.shape[0]).astype(bool) + for i in range(ijk.shape[0]): + for j in range(i+1, ijk.shape[0]): + if keep_idx[i] == 1: + dist = np.linalg.norm(ijk[i, :] - ijk[j, :]) + keep_idx[j] = dist > min_distance + ijk = ijk[keep_idx, :] + vals = vals[keep_idx] + return ijk, vals def _get_val(row, input_arr): @@ -129,18 +147,12 @@ def get_clusters_table(stat_img, stat_threshold, cluster_threshold=None): cluster_size_mm = int(np.sum(cluster_mask) * voxel_size) # Get peaks, subpeaks and associated statistics - subpeak_dist = int(np.round(8. / np.power(voxel_size, 1./3.))) # pylint: disable=no-member - subpeak_ijk = _local_max(masked_data, min_distance=subpeak_dist) - - subpeak_vals = np.apply_along_axis(arr=subpeak_ijk, axis=1, func1d=_get_val, - input_arr=masked_data) - order = (-subpeak_vals).argsort() - subpeak_vals = subpeak_vals[order] - subpeak_ijk = subpeak_ijk[order, :] - subpeak_xyz = np.asarray( - coord_transform( - subpeak_ijk[:, 0], subpeak_ijk[:, 1], subpeak_ijk[:, 2], - stat_img.affine)).tolist() + min_dist = int(np.round(8. / np.power(voxel_size, 1./3.))) # pylint: disable=no-member + subpeak_ijk, subpeak_vals = _local_max(masked_data, min_distance=min_dist) + subpeak_xyz = np.asarray(coord_transform(subpeak_ijk[:, 0], + subpeak_ijk[:, 1], + subpeak_ijk[:, 2], + stat_img.affine)).tolist() subpeak_xyz = np.array(subpeak_xyz).T # Only report peak and, at most, top 3 subpeaks. @@ -151,7 +163,7 @@ def get_clusters_table(stat_img, stat_threshold, cluster_threshold=None): subpeak_xyz[subpeak, 2], subpeak_vals[subpeak], cluster_size_mm] else: # Subpeak naming convention is cluster num + letter (1a, 1b, etc.) - sp_id = '{0}{1}'.format(c_id+1, ascii_lowercase[subpeak-1]) + sp_id = '{0}{1}'.format(c_id + 1, ascii_lowercase[subpeak - 1]) row = [sp_id, subpeak_xyz[subpeak, 0], subpeak_xyz[subpeak, 1], subpeak_xyz[subpeak, 2], subpeak_vals[subpeak], ''] rows += [row] @@ -275,7 +287,7 @@ def plot_design_matrix(design_matrix, rescale=True, ax=None): # normalize the values per column for better visualization _, X, names = check_design_matrix(design_matrix) if rescale: - X = X / np.maximum(1.e-12, np.sqrt(np.sum(X ** 2, 0))) + X = X / np.maximum(1.e-12, np.sqrt(np.sum(X ** 2, 0))) # pylint: disable=no-member if ax is None: plt.figure() ax = plt.subplot(1, 1, 1) diff --git a/nistats/tests/test_reporting.py b/nistats/tests/test_reporting.py index f7e3f182..8962aff3 100644 --- a/nistats/tests/test_reporting.py +++ b/nistats/tests/test_reporting.py @@ -1,5 +1,5 @@ from nistats.design_matrix import make_design_matrix -from nistats.reporting import plot_design_matrix, get_clusters_table +from nistats.reporting import plot_design_matrix, get_clusters_table, _local_max import nibabel as nib import numpy as np from numpy.testing import dec @@ -29,6 +29,23 @@ def test_show_design_matrix(): assert (ax is not None) +def test_local_max(): + shape = (9, 10, 11) + data = np.zeros(shape) + # Two maxima (one global, one local), 10 voxels apart. + data[4, 5, :] = [4, 3, 2, 1, 1, 1, 1, 1, 2, 3, 4] + data[5, 5, :] = [5, 4, 3, 2, 1, 1, 1, 2, 3, 4, 6] + data[6, 5, :] = [4, 3, 2, 1, 1, 1, 1, 1, 2, 3, 4] + + ijk, vals = _local_max(data, 9) + assert_true(np.array_equal(ijk, np.array([[5., 5., 10.], [5., 5., 0.]]))) + assert_true(np.array_equal(vals, np.array([6, 5]))) + + ijk, vals = _local_max(data, 11) + assert_true(np.array_equal(ijk, np.array([[5., 5., 10.]]))) + assert_true(np.array_equal(vals, np.array([6]))) + + def test_get_clusters_table(): shape = (9, 10, 11) data = np.zeros(shape) From a82e612eabc4da2b121fedd5e9f5859fc2b3d044 Mon Sep 17 00:00:00 2001 From: Gilles de Hollander Date: Wed, 14 Mar 2018 15:00:46 +0100 Subject: [PATCH 010/210] Fixed SimpleRegressionResults --- nistats/regression.py | 50 +++++++++++++++---------------------------- 1 file changed, 17 insertions(+), 33 deletions(-) diff --git a/nistats/regression.py b/nistats/regression.py index 081fdffc..a9156a62 100644 --- a/nistats/regression.py +++ b/nistats/regression.py @@ -282,15 +282,16 @@ def __init__(self, theta, Y, model, wY, wresid, cov=None, dispersion=1., dispersion, nuisance) self.wY = wY self.wresid = wresid + self.wdesign = model.wdesign - @setattr_on_read + @property def resid(self): """ Residuals from the fit. """ return self.Y - self.predicted - @setattr_on_read + @property def norm_resid(self): """ Residuals, normalized to have unit length. @@ -310,28 +311,28 @@ def norm_resid(self): """ return self.resid * pos_recipr(np.sqrt(self.dispersion)) - @setattr_on_read + @property def predicted(self): """ Return linear predictor values from a design matrix. """ beta = self.theta # the LikelihoodModelResults has parameters named 'theta' - X = self.model.design + X = self.wdesign return np.dot(X, beta) - @setattr_on_read + @property def SSE(self): """Error sum of squares. If not from an OLS model this is "pseudo"-SSE. """ return (self.wresid ** 2).sum(0) - @setattr_on_read + @property def MSE(self): """ Mean square (error) """ return self.SSE / self.df_resid -class SimpleRegressionResults(LikelihoodModelResults): +class SimpleRegressionResults(RegressionResults): """This class contains only information of the model fit necessary for contast computation. @@ -353,41 +354,24 @@ def __init__(self, results): # put this as a parameter of LikelihoodModel self.df_resid = self.df_total - self.df_model + self.wdesign = results.model.wdesign + def logL(self, Y): """ The maximized log-likelihood """ - raise ValueError('can not use this method for simple results') + raise ValueError("SimpleRegressionResult does not store residuals." + "And can therefore not calculate logL." + "If needed, use the RegressionResults class.") def resid(self, Y): """ Residuals from the fit. """ - return Y - self.predicted + raise ValueError("SimpleRegressionResult does not store residuals." + "If needed, use the RegressionResults class.") def norm_resid(self, Y): - """ - Residuals, normalized to have unit length. - - Notes - ----- - Is this supposed to return "stanardized residuals," - residuals standardized - to have mean zero and approximately unit variance? - - d_i = e_i / sqrt(MS_E) + raise ValueError("SimpleRegressionResult does not store residuals." + "If needed, use the RegressionResults class.") - Where MS_E = SSE / (n - k) - - See: Montgomery and Peck 3.2.1 p. 68 - Davidson and MacKinnon 15.2 p 662 - """ - return self.resid(Y) * pos_recipr(np.sqrt(self.dispersion)) - - def predicted(self): - """ Return linear predictor values from a design matrix. - """ - beta = self.theta - # the LikelihoodModelResults has parameters named 'theta' - X = self.model.design - return np.dot(X, beta) From b45b294cccd4963cfd700f1cf79907cf14d07f20 Mon Sep 17 00:00:00 2001 From: Gilles de Hollander Date: Wed, 14 Mar 2018 16:00:40 +0100 Subject: [PATCH 011/210] Make resid() and norm_resid() in SimpleRegressionResults @property, for consistence --- nistats/regression.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/nistats/regression.py b/nistats/regression.py index a9156a62..88e3e842 100644 --- a/nistats/regression.py +++ b/nistats/regression.py @@ -364,14 +364,16 @@ def logL(self, Y): "And can therefore not calculate logL." "If needed, use the RegressionResults class.") - def resid(self, Y): + @property + def resid(self): """ Residuals from the fit. """ raise ValueError("SimpleRegressionResult does not store residuals." "If needed, use the RegressionResults class.") - def norm_resid(self, Y): + @property + def norm_resid(self): raise ValueError("SimpleRegressionResult does not store residuals." "If needed, use the RegressionResults class.") From 9de71c66c7137ed9074480ee9208dde5485eca84 Mon Sep 17 00:00:00 2001 From: Gilles de Hollander Date: Wed, 14 Mar 2018 16:21:01 +0100 Subject: [PATCH 012/210] Fixed signal_scaling --- nistats/first_level_model.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/nistats/first_level_model.py b/nistats/first_level_model.py index e871fa57..cc15ac8f 100644 --- a/nistats/first_level_model.py +++ b/nistats/first_level_model.py @@ -287,15 +287,17 @@ def __init__(self, t_r=None, slice_time_ref=0., hrf_model='glover', self.memory = memory self.memory_level = memory_level self.standardize = standardize - if signal_scaling in [0, 1, (0, 1)]: + + if signal_scaling is False: + self.signal_scaling = signal_scaling + elif (type(signal_scaling) is not bool) and (signal_scaling in [0, 1, (0, 1)]): self.scaling_axis = signal_scaling self.signal_scaling = True self.standardize = False - elif signal_scaling is False: - self.signal_scaling = signal_scaling else: raise ValueError('signal_scaling must be "False", "0", "1"' ' or "(0, 1)"') + self.noise_model = noise_model self.verbose = verbose self.n_jobs = n_jobs From 2cd2f45e88716dacb3152157236d71fed9436fc6 Mon Sep 17 00:00:00 2001 From: Gilles de Hollander Date: Mon, 19 Mar 2018 13:24:19 +0100 Subject: [PATCH 013/210] Test files should not be in commit --- nistats/tests/dmtx_0.csv | 16 ---------------- nistats/tests/dmtx_1.csv | 17 ----------------- nistats/tests/fmri_run0.nii | Bin 47392 -> 0 bytes nistats/tests/fmri_run1.nii | Bin 50528 -> 0 bytes nistats/tests/mask.nii | Bin 744 -> 0 bytes 5 files changed, 33 deletions(-) delete mode 100644 nistats/tests/dmtx_0.csv delete mode 100644 nistats/tests/dmtx_1.csv delete mode 100644 nistats/tests/fmri_run0.nii delete mode 100644 nistats/tests/fmri_run1.nii delete mode 100644 nistats/tests/mask.nii diff --git a/nistats/tests/dmtx_0.csv b/nistats/tests/dmtx_0.csv deleted file mode 100644 index 175cf7e4..00000000 --- a/nistats/tests/dmtx_0.csv +++ /dev/null @@ -1,16 +0,0 @@ -,,, -0,1.65338484997,0.622980137291,0.882534713322 -1,-0.438900543877,-0.828594390115,0.00266177734877 -2,-0.917556105388,1.86821803136,0.0301041717254 -3,0.192657398378,1.47028470186,-1.71104568314 -4,0.963529993065,1.42248696189,-0.0626139987807 -5,0.053725206611,-0.070010156494,-1.41046541121 -6,-0.510259550383,1.97300426691,0.404999664427 -7,0.546664946831,1.79553259519,-1.66669036046 -8,1.00673627307,1.12843993346,0.0428649466597 -9,1.01660637147,-1.08854405265,-1.06275436577 -10,-1.71746790696,-0.150027071448,-0.733401144959 -11,-0.179607639826,-0.343257247782,-1.50355734566 -12,-1.50424762872,1.82606605007,-0.0198921251895 -13,1.50606358221,1.84383254812,0.168853046754 -14,0.568632846981,-0.909918675584,-0.133375099571 diff --git a/nistats/tests/dmtx_1.csv b/nistats/tests/dmtx_1.csv deleted file mode 100644 index 0b6f9efb..00000000 --- a/nistats/tests/dmtx_1.csv +++ /dev/null @@ -1,17 +0,0 @@ -,,, -0,-0.973936302151,-0.417711973277,0.270939828652 -1,0.808859074577,0.691763719026,0.941520837379 -2,0.705606271002,0.253935426538,0.823944820177 -3,1.13588431266,0.77775084327,-0.858697593472 -4,0.124826461198,0.737902923978,-0.423492210605 -5,0.377363290219,-1.58868166796,0.238635677853 -6,1.21916209617,0.859981825339,0.490378538851 -7,0.161201567658,1.02122802132,-1.40048031466 -8,-1.62601401965,0.446830029008,-0.792344847654 -9,-1.31489402612,1.08790355567,0.201338849493 -10,1.41620899598,0.357438830237,-1.46322151379 -11,0.43889070876,1.15399663639,-0.846522660308 -12,-1.01812664868,-2.39718666768,0.621688713329 -13,-1.05992187833,1.78868386456,1.31874110751 -14,-0.474053643864,1.58366804783,-0.0230456672759 -15,-0.21593079551,0.511885699364,-0.0753417002309 diff --git a/nistats/tests/fmri_run0.nii b/nistats/tests/fmri_run0.nii deleted file mode 100644 index 60d5e715a70178010b8e8df6778563eaddfc2e9a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 47392 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 Hom2RK!gq!L diff --git a/nistats/tests/fmri_run1.nii b/nistats/tests/fmri_run1.nii deleted file mode 100644 index 491c6353c98c65a28a66149003ba53eff36fc429..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 50528 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 AdH?_b diff --git a/nistats/tests/mask.nii b/nistats/tests/mask.nii deleted file mode 100644 index 5ec1eafc056258a6124546ce8c057a4d4f48c6cd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 744 zcmc(b%Mrpb3`8Y3kX|@Z0SDTk25O)rN>y7=;g?|vwDTf6klQKfrpjTDRVTX{2 zyje^N6LMyS_=xGFbZ3l0TOCjl8+A|T9mdazPsgfsZLR}sVx`!@Q9c@IIXYCXYzLcN SBZ+)TjHP062oPV2T?_gE From c1ddb817fa5152a1750f061211391f3e25b33981 Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Thu, 29 Mar 2018 08:36:26 -0400 Subject: [PATCH 014/210] Fix examples and docs. --- doc/modules/reference.rst | 2 +- examples/01_tutorials/plot_bids_features.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/modules/reference.rst b/doc/modules/reference.rst index e49d5f57..1b3cb41a 100644 --- a/doc/modules/reference.rst +++ b/doc/modules/reference.rst @@ -243,7 +243,6 @@ uses. fdr_threshold map_threshold - get_clusters_table .. _reporting_ref: @@ -265,6 +264,7 @@ uses. compare_niimgs plot_design_matrix plot_contrast_matrix + get_clusters_table .. _utils_ref: diff --git a/examples/01_tutorials/plot_bids_features.py b/examples/01_tutorials/plot_bids_features.py index fef976bd..40ad117c 100644 --- a/examples/01_tutorials/plot_bids_features.py +++ b/examples/01_tutorials/plot_bids_features.py @@ -141,5 +141,5 @@ ############################################################################### # We can get a latex table from a Pandas Dataframe for display and publication -from nistats.thresholding import get_clusters_table +from nistats.reporting import get_clusters_table print(get_clusters_table(z_map, norm.isf(0.001), 10).to_latex()) From 36460ca0b3142c204543eb7ae4749e0b21eca941 Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Sat, 12 May 2018 14:16:50 -0400 Subject: [PATCH 015/210] Address review of get_clusters_table - Use minimum distance in millimeters in _local_max. - Requires including the image affine in the arguments as well. - Should also be robust to non-isotropic voxels. - Add min_distance argument to get_clusters_table, for minimum distance in millimeters. Default is 8mm. - Address PEP8 problems. --- nistats/reporting.py | 45 ++++++++++++++++++++------------- nistats/tests/test_reporting.py | 8 +++--- 2 files changed, 33 insertions(+), 20 deletions(-) diff --git a/nistats/reporting.py b/nistats/reporting.py index 948d633a..7988355b 100644 --- a/nistats/reporting.py +++ b/nistats/reporting.py @@ -17,11 +17,12 @@ import matplotlib.pyplot as plt from scipy import ndimage from patsy import DesignInfo +import nibabel as nib from .design_matrix import check_design_matrix -def _local_max(data, min_distance): +def _local_max(data, affine, min_distance): """Find all local maxima of the array, separated by at least min_distance. Adapted from https://stackoverflow.com/a/22631583/2589328 @@ -31,8 +32,7 @@ def _local_max(data, min_distance): 3D array of with masked values for cluster. min_distance : :obj:`int` - Minimum distance between local maxima in ``data``, in terms of - voxels (not mm). + Minimum distance between local maxima in ``data``, in terms of mm. Returns ------- @@ -42,27 +42,33 @@ def _local_max(data, min_distance): vals : :obj:`numpy.ndarray` (n_foci,) array of values from data at ijk. """ - data_max = ndimage.filters.maximum_filter(data, min_distance) + # Initial identification of subpeaks with minimal minimum distance + data_max = ndimage.filters.maximum_filter(data, 3) maxima = (data == data_max) - data_min = ndimage.filters.minimum_filter(data, min_distance) + data_min = ndimage.filters.minimum_filter(data, 3) diff = ((data_max - data_min) > 0) maxima[diff == 0] = 0 - labeled, num_objects = ndimage.label(maxima) - ijk = np.array(ndimage.center_of_mass(data, labeled, range(1, num_objects+1))) + labeled, n_subpeaks = ndimage.label(maxima) + ijk = np.array(ndimage.center_of_mass(data, labeled, + range(1, n_subpeaks + 1))) ijk = np.round(ijk).astype(int) - vals = np.apply_along_axis(arr=ijk, axis=1, func1d=_get_val, input_arr=data) + vals = np.apply_along_axis(arr=ijk, axis=1, func1d=_get_val, + input_arr=data) + + # Sort subpeaks in cluster in descending order of stat value order = (-vals).argsort() vals = vals[order] ijk = ijk[order, :] + xyz = nib.affines.apply_affine(affine, ijk) # Convert to xyz in mm # Reduce list of subpeaks based on distance - keep_idx = np.ones(ijk.shape[0]).astype(bool) - for i in range(ijk.shape[0]): - for j in range(i+1, ijk.shape[0]): + keep_idx = np.ones(xyz.shape[0]).astype(bool) + for i in range(xyz.shape[0]): + for j in range(i + 1, xyz.shape[0]): if keep_idx[i] == 1: - dist = np.linalg.norm(ijk[i, :] - ijk[j, :]) + dist = np.linalg.norm(xyz[i, :] - xyz[j, :]) keep_idx[j] = dist > min_distance ijk = ijk[keep_idx, :] vals = vals[keep_idx] @@ -76,7 +82,8 @@ def _get_val(row, input_arr): return input_arr[i, j, k] -def get_clusters_table(stat_img, stat_threshold, cluster_threshold=None): +def get_clusters_table(stat_img, stat_threshold, cluster_threshold=None, + min_distance=8.): """Creates pandas dataframe with img cluster statistics. Parameters @@ -91,6 +98,9 @@ def get_clusters_table(stat_img, stat_threshold, cluster_threshold=None): cluster_threshold : :obj:`int` or :obj:`None`, optional Cluster size threshold, in voxels. + min_distance: :obj:`float`, optional + Minimum distance between subpeaks in mm. Default is 8 mm. + Returns ------- df : :obj:`pandas.DataFrame` @@ -146,8 +156,8 @@ def get_clusters_table(stat_img, stat_threshold, cluster_threshold=None): cluster_size_mm = int(np.sum(cluster_mask) * voxel_size) # Get peaks, subpeaks and associated statistics - min_dist = int(np.round(8. / np.power(voxel_size, 1./3.))) # pylint: disable=no-member - subpeak_ijk, subpeak_vals = _local_max(masked_data, min_distance=min_dist) + subpeak_ijk, subpeak_vals = _local_max(masked_data, stat_img.affine, + min_distance=min_distance) subpeak_xyz = np.asarray(coord_transform(subpeak_ijk[:, 0], subpeak_ijk[:, 1], subpeak_ijk[:, 2], @@ -158,8 +168,9 @@ def get_clusters_table(stat_img, stat_threshold, cluster_threshold=None): n_subpeaks = np.min((len(subpeak_vals), 4)) for subpeak in range(n_subpeaks): if subpeak == 0: - row = [c_id+1, subpeak_xyz[subpeak, 0], subpeak_xyz[subpeak, 1], - subpeak_xyz[subpeak, 2], subpeak_vals[subpeak], cluster_size_mm] + row = [c_id + 1, subpeak_xyz[subpeak, 0], + subpeak_xyz[subpeak, 1], subpeak_xyz[subpeak, 2], + subpeak_vals[subpeak], cluster_size_mm] else: # Subpeak naming convention is cluster num + letter (1a, 1b, etc.) sp_id = '{0}{1}'.format(c_id + 1, ascii_lowercase[subpeak - 1]) diff --git a/nistats/tests/test_reporting.py b/nistats/tests/test_reporting.py index 8962aff3..6132426d 100644 --- a/nistats/tests/test_reporting.py +++ b/nistats/tests/test_reporting.py @@ -1,5 +1,6 @@ from nistats.design_matrix import make_design_matrix -from nistats.reporting import plot_design_matrix, get_clusters_table, _local_max +from nistats.reporting import (plot_design_matrix, get_clusters_table, + _local_max) import nibabel as nib import numpy as np from numpy.testing import dec @@ -36,12 +37,13 @@ def test_local_max(): data[4, 5, :] = [4, 3, 2, 1, 1, 1, 1, 1, 2, 3, 4] data[5, 5, :] = [5, 4, 3, 2, 1, 1, 1, 2, 3, 4, 6] data[6, 5, :] = [4, 3, 2, 1, 1, 1, 1, 1, 2, 3, 4] + affine = np.eye(4) - ijk, vals = _local_max(data, 9) + ijk, vals = _local_max(data, affine, min_distance=9) assert_true(np.array_equal(ijk, np.array([[5., 5., 10.], [5., 5., 0.]]))) assert_true(np.array_equal(vals, np.array([6, 5]))) - ijk, vals = _local_max(data, 11) + ijk, vals = _local_max(data, affine, min_distance=11) assert_true(np.array_equal(ijk, np.array([[5., 5., 10.]]))) assert_true(np.array_equal(vals, np.array([6]))) From ab542008425327dc5657b0da68776769445c15b8 Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Mon, 21 May 2018 18:27:50 -0400 Subject: [PATCH 016/210] Group scipy imports together to trigger CI --- nistats/reporting.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nistats/reporting.py b/nistats/reporting.py index 7988355b..e9400cd3 100644 --- a/nistats/reporting.py +++ b/nistats/reporting.py @@ -10,14 +10,14 @@ import numpy as np import pandas as pd +import nibabel as nib from scipy import stats +from scipy import ndimage import nilearn.plotting # overrides the backend on headless servers from nilearn.image.resampling import coord_transform import matplotlib import matplotlib.pyplot as plt -from scipy import ndimage from patsy import DesignInfo -import nibabel as nib from .design_matrix import check_design_matrix From 03b42148c764e3964f52bd03df9ee02fa170cb91 Mon Sep 17 00:00:00 2001 From: bthirion Date: Fri, 6 Apr 2018 15:47:14 +0200 Subject: [PATCH 017/210] introduced safeF contrast --- nistats/contrasts.py | 64 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 51 insertions(+), 13 deletions(-) diff --git a/nistats/contrasts.py b/nistats/contrasts.py index 7bc4f275..86ef03d9 100644 --- a/nistats/contrasts.py +++ b/nistats/contrasts.py @@ -48,29 +48,45 @@ def compute_contrast(labels, regression_result, con_val, contrast_type=None): if contrast_type is None: contrast_type = 't' if dim == 1 else 'F' - acceptable_contrast_types = ['t', 'F'] + acceptable_contrast_types = ['t', 'F', 'safe_F'] if contrast_type not in acceptable_contrast_types: raise ValueError( '"{0}" is not a known contrast type. Allowed types are {1}'. format(contrast_type, acceptable_contrast_types)) - effect_ = np.zeros((dim, labels.size)) - var_ = np.zeros((dim, dim, labels.size)) if contrast_type == 't': + effect_ = np.zeros((1, labels.size)) + var_ = np.zeros((1, 1, labels.size)) for label_ in regression_result: label_mask = labels == label_ resl = regression_result[label_].Tcontrast(con_val) effect_[:, label_mask] = resl.effect.T var_[:, :, label_mask] = (resl.sd ** 2).T - else: + elif contrast_type == 'F': + effect_ = np.zeros((dim, labels.size)) + var_ = np.zeros((dim, dim, labels.size)) for label_ in regression_result: label_mask = labels == label_ resl = regression_result[label_].Fcontrast(con_val) effect_[:, label_mask] = resl.effect var_[:, :, label_mask] = resl.covariance + else: + effect_ = np.zeros((1, labels.size)) + var_ = np.zeros((1, 1, labels.size)) + for label_ in regression_result: + label_mask = labels == label_ + reg = regression_result[label_] + + ctheta = np.dot(con_val, reg.theta) + invcov = np.linalg.inv(reg.vcov(matrix=con_val, dispersion=1.0)) + ess = np.sum(np.dot(ctheta.T, invcov) * ctheta.T, 1) /\ + invcov.shape[0] + rss = reg.dispersion + effect_[:, label_mask] = np.sqrt(ess) + var_[:, :, label_mask] = rss dof_ = regression_result[label_].df_resid - return Contrast(effect=effect_, variance=var_, dof=dof_, + return Contrast(effect=effect_, variance=var_, dim=dim, dof=dof_, contrast_type=contrast_type) @@ -105,7 +121,7 @@ class Contrast(object): (high-dimensional F constrasts may lead to memory breakage). """ - def __init__(self, effect, variance, dof=DEF_DOFMAX, contrast_type='t', + def __init__(self, effect, variance, dim=None, dof=DEF_DOFMAX, contrast_type='t', tiny=DEF_TINY, dofmax=DEF_DOFMAX): """ Parameters @@ -117,7 +133,7 @@ def __init__(self, effect, variance, dof=DEF_DOFMAX, contrast_type='t', the associated variance estimate dof : scalar - the degrees of freedom of the resiudals + the degrees of freedom of the residuals contrast_type: {'t', 'F'} specification of the contrast type @@ -135,7 +151,10 @@ def __init__(self, effect, variance, dof=DEF_DOFMAX, contrast_type='t', self.effect = effect self.variance = variance self.dof = float(dof) - self.dim = effect.shape[0] + if dim is None: + self.dim = effect.shape[0] + else: + self.dim = dim if self.dim > 1 and contrast_type is 't': print('Automatically converted multi-dimensional t to F contrast') contrast_type = 'F' @@ -173,7 +192,10 @@ def stat(self, baseline=0.0): self.baseline = baseline # Case: one-dimensional contrast ==> t or t**2 - if self.dim == 1: + if self.contrast_type == 'safe_F': + stat = (self.effect - baseline) ** 2 /\ + np.maximum(self.variance, self.tiny) + elif self.dim == 1: # avoids division by zero stat = (self.effect - baseline) / np.sqrt( np.maximum(self.variance, self.tiny)) @@ -213,7 +235,8 @@ def p_value(self, baseline=0.0): # Valid conjunction as in Nichols et al, Neuroimage 25, 2005. if self.contrast_type == 't': p_values = sps.t.sf(self.stat_, np.minimum(self.dof, self.dofmax)) - elif self.contrast_type == 'F': + elif self.contrast_type in ['F', 'safe_F']: + print(self.dim, self.dof, self.dofmax) p_values = sps.f.sf(self.stat_, self.dim, np.minimum( self.dof, self.dofmax)) else: @@ -252,11 +275,26 @@ def __add__(self, other): if self.dim != other.dim: raise ValueError( 'The two contrasts do not have compatible dimensions') + dof_ = self.dof + other.dof + if self.contrast_type == 'safe_F': + warn('Running fixed effects on F statistics. As Stoufer method \ + is used, only the p-values, stat and z_score make sense') + """z_score_ = (self.z_score() + other.z_score()) / np.sqrt(2) + p_values = sps.norm.sf(z_score_) + stat = sps.f.isf(p_values, self.dim, np.minimum( + dof_, self.dofmax)) + effect_ = np.sqrt(stat)[np.newaxis] + variance_ = np.ones_like(effect_)[np.newaxis] + """ + #variance_ = 1. / (1. / self.variance + 1. / other.variance) + #effect_ = (self.effect / self.variance + + # other.effect / other.variance) * variance_ + #effect_ = effect_.reshape(self.effect.shape) + effect_ = self.effect + other.effect variance_ = self.variance + other.variance - dof_ = self.dof + other.dof - return Contrast(effect=effect_, variance=variance_, dof=dof_, - contrast_type=self.contrast_type) + return Contrast(effect=effect_, variance=variance_, dim=self.dim, + dof=dof_, contrast_type=self.contrast_type) def __rmul__(self, scalar): """Multiplication of the contrast by a scalar""" From 337365aef00932cd11a9a2fd8930bc27ee77f512 Mon Sep 17 00:00:00 2001 From: bthirion Date: Fri, 6 Apr 2018 15:49:24 +0200 Subject: [PATCH 018/210] Cleaning --- nistats/contrasts.py | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/nistats/contrasts.py b/nistats/contrasts.py index 86ef03d9..2ac25efe 100644 --- a/nistats/contrasts.py +++ b/nistats/contrasts.py @@ -277,20 +277,8 @@ def __add__(self, other): 'The two contrasts do not have compatible dimensions') dof_ = self.dof + other.dof if self.contrast_type == 'safe_F': - warn('Running fixed effects on F statistics. As Stoufer method \ - is used, only the p-values, stat and z_score make sense') - """z_score_ = (self.z_score() + other.z_score()) / np.sqrt(2) - p_values = sps.norm.sf(z_score_) - stat = sps.f.isf(p_values, self.dim, np.minimum( - dof_, self.dofmax)) - effect_ = np.sqrt(stat)[np.newaxis] - variance_ = np.ones_like(effect_)[np.newaxis] - """ - #variance_ = 1. / (1. / self.variance + 1. / other.variance) - #effect_ = (self.effect / self.variance + - # other.effect / other.variance) * variance_ - #effect_ = effect_.reshape(self.effect.shape) - + warn('Running fixed effects on F statistics. As an approximation " + "is used, the results are only indicative') effect_ = self.effect + other.effect variance_ = self.variance + other.variance return Contrast(effect=effect_, variance=variance_, dim=self.dim, From ccd6928d16488deaf2cab5dee40da3031af5a5b7 Mon Sep 17 00:00:00 2001 From: bthirion Date: Fri, 6 Apr 2018 16:16:02 +0200 Subject: [PATCH 019/210] Improved formula for SafeF --- .../plot_fixed_effects.py | 97 +++++++++++++++++++ nistats/contrasts.py | 23 +++-- 2 files changed, 108 insertions(+), 12 deletions(-) create mode 100644 examples/02_first_level_models/plot_fixed_effects.py diff --git a/examples/02_first_level_models/plot_fixed_effects.py b/examples/02_first_level_models/plot_fixed_effects.py new file mode 100644 index 00000000..8eb57f31 --- /dev/null +++ b/examples/02_first_level_models/plot_fixed_effects.py @@ -0,0 +1,97 @@ +""" +Simple example of GLM fitting in fMRI +====================================== + +Full step-by-step example of fitting a GLM to experimental data and visualizing +the results. This is done on two runs of one subject of the FIAC dataset. +For details on the data, please see: + +Dehaene-Lambertz G, Dehaene S, Anton JL, Campagne A, Ciuciu P, Dehaene +G, Denghien I, Jobert A, LeBihan D, Sigman M, Pallier C, Poline +JB. Functional segregation of cortical language areas by sentence +repetition. Hum Brain Mapp. 2006: 27:360--371. +http://www.pubmedcentral.nih.gov/articlerender.fcgi?artid=2653076#R11 + +More specifically: + +1. A sequence of fMRI volumes are loaded +2. A design matrix describing all the effects related to the data is computed +3. a mask of the useful brain volume is computed +4. A GLM is applied to the dataset (effect/covariance, + then contrast estimation) + +""" +from os import mkdir, path, getcwd + +import numpy as np +import pandas as pd + +from nilearn import plotting +from nilearn.image import mean_img + +from nistats.first_level_model import FirstLevelModel +from nistats import datasets + + +# write directory +write_dir = path.join(getcwd(), 'results') +if not path.exists(write_dir): + mkdir(write_dir) + +######################################################################### +# Prepare data and analysis parameters +# -------------------------------------- +data = datasets.fetch_fiac_first_level() +fmri_img = [data['func1'], data['func2']] +mean_img_ = mean_img(fmri_img[0]) +design_files = [data['design_matrix1'], data['design_matrix2']] +design_matrices = [pd.DataFrame(np.load(df)['X']) for df in design_files] + +######################################################################### +# GLM estimation +# ---------------------------------- +# GLM specification +fmri_glm = FirstLevelModel(mask=data['mask'], minimize_memory=True) + +######################################################################### +# GLM fitting +fmri_glm = fmri_glm.fit(fmri_img, design_matrices=design_matrices) + +######################################################################### +# compute fixed effects of the two runs and compute related images +n_columns = design_matrices[0].shape[1] + + +def pad_vector(contrast_, n_columns): + return np.hstack((contrast_, np.zeros(n_columns - len(contrast_)))) + + +contrasts = { + 'Effects_of_interest': np.eye(n_columns)[:5] + } + + +print('Computing contrasts...') +for index, (contrast_id, contrast_val) in enumerate(contrasts.items()): + # contrast_val = contrast_val[np.newaxis] + output_type = 'z_score' + z_map = fmri_glm.compute_contrast( + contrast_val, output_type=output_type, stat_type='F') + # + display = plotting.plot_stat_map( + z_map, bg_img=mean_img_, threshold=2.5, title=contrast_id, vmax=10) + display.savefig(path.join(write_dir, '%s_z_map.png' % contrast_id)) + # + z_map_safe = fmri_glm.compute_contrast( + contrast_val, output_type=output_type, stat_type='safe_F') + display = plotting.plot_stat_map( + z_map_safe, bg_img=mean_img_, threshold=2.5, title=contrast_id, + vmax=10) + + +import matplotlib.pyplot as plt +plt.figure() +z1 = np.ravel(z_map.get_data()) +z2 = np.ravel(z_map_safe.get_data()) +plt.plot(z1, z2, '.') +plt.show() diff --git a/nistats/contrasts.py b/nistats/contrasts.py index 2ac25efe..346849a1 100644 --- a/nistats/contrasts.py +++ b/nistats/contrasts.py @@ -71,18 +71,17 @@ def compute_contrast(labels, regression_result, con_val, contrast_type=None): effect_[:, label_mask] = resl.effect var_[:, :, label_mask] = resl.covariance else: - effect_ = np.zeros((1, labels.size)) + from scipy.linalg import sqrtm + effect_ = np.zeros((dim, labels.size)) var_ = np.zeros((1, 1, labels.size)) for label_ in regression_result: label_mask = labels == label_ reg = regression_result[label_] - - ctheta = np.dot(con_val, reg.theta) + cbeta = np.dot(con_val, reg.theta) invcov = np.linalg.inv(reg.vcov(matrix=con_val, dispersion=1.0)) - ess = np.sum(np.dot(ctheta.T, invcov) * ctheta.T, 1) /\ - invcov.shape[0] + wcbeta = np.dot(sqrtm(invcov), cbeta) rss = reg.dispersion - effect_[:, label_mask] = np.sqrt(ess) + effect_[:, label_mask] = wcbeta var_[:, :, label_mask] = rss dof_ = regression_result[label_].df_resid @@ -144,9 +143,9 @@ def __init__(self, effect, variance, dim=None, dof=DEF_DOFMAX, contrast_type='t' raise ValueError('Effect array should have 2 dimensions') if variance.shape[0] != variance.shape[1]: raise ValueError('Inconsistent shape for the variance estimate') - if ((variance.shape[1] != effect.shape[0]) or - (variance.shape[2] != effect.shape[1])): - raise ValueError('Effect and variance have inconsistent shape') + #if ((variance.shape[1] != effect.shape[0]) or + # (variance.shape[2] != effect.shape[1])): + # raise ValueError('Effect and variance have inconsistent shape') self.effect = effect self.variance = variance @@ -193,7 +192,7 @@ def stat(self, baseline=0.0): # Case: one-dimensional contrast ==> t or t**2 if self.contrast_type == 'safe_F': - stat = (self.effect - baseline) ** 2 /\ + stat = np.sum((self.effect - baseline) ** 2, 0) / self.dim /\ np.maximum(self.variance, self.tiny) elif self.dim == 1: # avoids division by zero @@ -277,8 +276,8 @@ def __add__(self, other): 'The two contrasts do not have compatible dimensions') dof_ = self.dof + other.dof if self.contrast_type == 'safe_F': - warn('Running fixed effects on F statistics. As an approximation " - "is used, the results are only indicative') + warn('Running fixed effects on F statistics.' + \ + 'As an approximation is used, the results are only indicative') effect_ = self.effect + other.effect variance_ = self.variance + other.variance return Contrast(effect=effect_, variance=variance_, dim=self.dim, From bd0c920b1880ad0f96a49761af74673ac3da1273 Mon Sep 17 00:00:00 2001 From: bthirion Date: Fri, 6 Apr 2018 16:29:01 +0200 Subject: [PATCH 020/210] changed the F contrast to the new one --- nistats/contrasts.py | 42 +++++++++--------------------------------- 1 file changed, 9 insertions(+), 33 deletions(-) diff --git a/nistats/contrasts.py b/nistats/contrasts.py index 346849a1..5b0b89ff 100644 --- a/nistats/contrasts.py +++ b/nistats/contrasts.py @@ -48,7 +48,7 @@ def compute_contrast(labels, regression_result, con_val, contrast_type=None): if contrast_type is None: contrast_type = 't' if dim == 1 else 'F' - acceptable_contrast_types = ['t', 'F', 'safe_F'] + acceptable_contrast_types = ['t', 'F'] if contrast_type not in acceptable_contrast_types: raise ValueError( '"{0}" is not a known contrast type. Allowed types are {1}'. @@ -63,22 +63,15 @@ def compute_contrast(labels, regression_result, con_val, contrast_type=None): effect_[:, label_mask] = resl.effect.T var_[:, :, label_mask] = (resl.sd ** 2).T elif contrast_type == 'F': - effect_ = np.zeros((dim, labels.size)) - var_ = np.zeros((dim, dim, labels.size)) - for label_ in regression_result: - label_mask = labels == label_ - resl = regression_result[label_].Fcontrast(con_val) - effect_[:, label_mask] = resl.effect - var_[:, :, label_mask] = resl.covariance - else: from scipy.linalg import sqrtm effect_ = np.zeros((dim, labels.size)) var_ = np.zeros((1, 1, labels.size)) for label_ in regression_result: label_mask = labels == label_ reg = regression_result[label_] - cbeta = np.dot(con_val, reg.theta) - invcov = np.linalg.inv(reg.vcov(matrix=con_val, dispersion=1.0)) + cbeta = np.atleast_2d(np.dot(con_val, reg.theta)) + invcov = np.linalg.inv(np.atleast_2d( + reg.vcov(matrix=con_val, dispersion=1.0))) wcbeta = np.dot(sqrtm(invcov), cbeta) rss = reg.dispersion effect_[:, label_mask] = wcbeta @@ -143,9 +136,6 @@ def __init__(self, effect, variance, dim=None, dof=DEF_DOFMAX, contrast_type='t' raise ValueError('Effect array should have 2 dimensions') if variance.shape[0] != variance.shape[1]: raise ValueError('Inconsistent shape for the variance estimate') - #if ((variance.shape[1] != effect.shape[0]) or - # (variance.shape[2] != effect.shape[1])): - # raise ValueError('Effect and variance have inconsistent shape') self.effect = effect self.variance = variance @@ -191,25 +181,13 @@ def stat(self, baseline=0.0): self.baseline = baseline # Case: one-dimensional contrast ==> t or t**2 - if self.contrast_type == 'safe_F': + if self.contrast_type == 'F': stat = np.sum((self.effect - baseline) ** 2, 0) / self.dim /\ np.maximum(self.variance, self.tiny) - elif self.dim == 1: + elif self.contrast_type == 't': # avoids division by zero stat = (self.effect - baseline) / np.sqrt( np.maximum(self.variance, self.tiny)) - if self.contrast_type == 'F': - stat = stat ** 2 - # Case: F contrast - elif self.contrast_type == 'F': - # F = |t|^2/q , |t|^2 = e^t inv(v) e - if self.effect.ndim == 1: - self.effect = self.effect[np.newaxis] - if self.variance.ndim == 1: - self.variance = self.variance[np.newaxis, np.newaxis] - stat = (multiple_mahalanobis( - self.effect - baseline, self.variance) / self.dim) - # Unknwon stat else: raise ValueError('Unknown statistic type') self.stat_ = stat @@ -234,8 +212,7 @@ def p_value(self, baseline=0.0): # Valid conjunction as in Nichols et al, Neuroimage 25, 2005. if self.contrast_type == 't': p_values = sps.t.sf(self.stat_, np.minimum(self.dof, self.dofmax)) - elif self.contrast_type in ['F', 'safe_F']: - print(self.dim, self.dof, self.dofmax) + elif self.contrast_type == 'F': p_values = sps.f.sf(self.stat_, self.dim, np.minimum( self.dof, self.dofmax)) else: @@ -275,9 +252,8 @@ def __add__(self, other): raise ValueError( 'The two contrasts do not have compatible dimensions') dof_ = self.dof + other.dof - if self.contrast_type == 'safe_F': - warn('Running fixed effects on F statistics.' + \ - 'As an approximation is used, the results are only indicative') + if self.contrast_type == 'F': + warn('Running approximate fixed effects on F statistics.') effect_ = self.effect + other.effect variance_ = self.variance + other.variance return Contrast(effect=effect_, variance=variance_, dim=self.dim, From d66c4740245f1e12b4cde24b9318fa7ff7c51833 Mon Sep 17 00:00:00 2001 From: bthirion Date: Fri, 6 Apr 2018 23:20:23 +0200 Subject: [PATCH 021/210] removed temporary example --- .../plot_fixed_effects.py | 97 ------------------- 1 file changed, 97 deletions(-) delete mode 100644 examples/02_first_level_models/plot_fixed_effects.py diff --git a/examples/02_first_level_models/plot_fixed_effects.py b/examples/02_first_level_models/plot_fixed_effects.py deleted file mode 100644 index 8eb57f31..00000000 --- a/examples/02_first_level_models/plot_fixed_effects.py +++ /dev/null @@ -1,97 +0,0 @@ -""" -Simple example of GLM fitting in fMRI -====================================== - -Full step-by-step example of fitting a GLM to experimental data and visualizing -the results. This is done on two runs of one subject of the FIAC dataset. -For details on the data, please see: - -Dehaene-Lambertz G, Dehaene S, Anton JL, Campagne A, Ciuciu P, Dehaene -G, Denghien I, Jobert A, LeBihan D, Sigman M, Pallier C, Poline -JB. Functional segregation of cortical language areas by sentence -repetition. Hum Brain Mapp. 2006: 27:360--371. -http://www.pubmedcentral.nih.gov/articlerender.fcgi?artid=2653076#R11 - -More specifically: - -1. A sequence of fMRI volumes are loaded -2. A design matrix describing all the effects related to the data is computed -3. a mask of the useful brain volume is computed -4. A GLM is applied to the dataset (effect/covariance, - then contrast estimation) - -""" -from os import mkdir, path, getcwd - -import numpy as np -import pandas as pd - -from nilearn import plotting -from nilearn.image import mean_img - -from nistats.first_level_model import FirstLevelModel -from nistats import datasets - - -# write directory -write_dir = path.join(getcwd(), 'results') -if not path.exists(write_dir): - mkdir(write_dir) - -######################################################################### -# Prepare data and analysis parameters -# -------------------------------------- -data = datasets.fetch_fiac_first_level() -fmri_img = [data['func1'], data['func2']] -mean_img_ = mean_img(fmri_img[0]) -design_files = [data['design_matrix1'], data['design_matrix2']] -design_matrices = [pd.DataFrame(np.load(df)['X']) for df in design_files] - -######################################################################### -# GLM estimation -# ---------------------------------- -# GLM specification -fmri_glm = FirstLevelModel(mask=data['mask'], minimize_memory=True) - -######################################################################### -# GLM fitting -fmri_glm = fmri_glm.fit(fmri_img, design_matrices=design_matrices) - -######################################################################### -# compute fixed effects of the two runs and compute related images -n_columns = design_matrices[0].shape[1] - - -def pad_vector(contrast_, n_columns): - return np.hstack((contrast_, np.zeros(n_columns - len(contrast_)))) - - -contrasts = { - 'Effects_of_interest': np.eye(n_columns)[:5] - } - - -print('Computing contrasts...') -for index, (contrast_id, contrast_val) in enumerate(contrasts.items()): - # contrast_val = contrast_val[np.newaxis] - output_type = 'z_score' - z_map = fmri_glm.compute_contrast( - contrast_val, output_type=output_type, stat_type='F') - # - display = plotting.plot_stat_map( - z_map, bg_img=mean_img_, threshold=2.5, title=contrast_id, vmax=10) - display.savefig(path.join(write_dir, '%s_z_map.png' % contrast_id)) - # - z_map_safe = fmri_glm.compute_contrast( - contrast_val, output_type=output_type, stat_type='safe_F') - display = plotting.plot_stat_map( - z_map_safe, bg_img=mean_img_, threshold=2.5, title=contrast_id, - vmax=10) - - -import matplotlib.pyplot as plt -plt.figure() -z1 = np.ravel(z_map.get_data()) -z2 = np.ravel(z_map_safe.get_data()) -plt.plot(z1, z2, '.') -plt.show() From a0a6ddc49165a0d60e22a66696a6c5a1fc6cdec4 Mon Sep 17 00:00:00 2001 From: bthirion Date: Sun, 8 Apr 2018 22:12:53 +0200 Subject: [PATCH 022/210] added docstring for dim in contrast class --- nistats/contrasts.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/nistats/contrasts.py b/nistats/contrasts.py index 5b0b89ff..f229a3f8 100644 --- a/nistats/contrasts.py +++ b/nistats/contrasts.py @@ -124,6 +124,9 @@ def __init__(self, effect, variance, dim=None, dof=DEF_DOFMAX, contrast_type='t' variance : array of shape (contrast_dim, contrast_dim, n_voxels) the associated variance estimate + dim: int or None, + the dimension of the contrast + dof : scalar the degrees of freedom of the residuals @@ -212,7 +215,7 @@ def p_value(self, baseline=0.0): # Valid conjunction as in Nichols et al, Neuroimage 25, 2005. if self.contrast_type == 't': p_values = sps.t.sf(self.stat_, np.minimum(self.dof, self.dofmax)) - elif self.contrast_type == 'F': + elif self.contrast_type == 'F': p_values = sps.f.sf(self.stat_, self.dim, np.minimum( self.dof, self.dofmax)) else: From 2efe691fe421f15677a8e652433080e8ef0de398 Mon Sep 17 00:00:00 2001 From: bthirion Date: Tue, 29 May 2018 15:08:04 +0200 Subject: [PATCH 023/210] sqeeze variance --- nistats/contrasts.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/nistats/contrasts.py b/nistats/contrasts.py index f229a3f8..fb5f80cd 100644 --- a/nistats/contrasts.py +++ b/nistats/contrasts.py @@ -56,16 +56,16 @@ def compute_contrast(labels, regression_result, con_val, contrast_type=None): if contrast_type == 't': effect_ = np.zeros((1, labels.size)) - var_ = np.zeros((1, 1, labels.size)) + var_ = np.zeros(labels.size) for label_ in regression_result: label_mask = labels == label_ resl = regression_result[label_].Tcontrast(con_val) effect_[:, label_mask] = resl.effect.T - var_[:, :, label_mask] = (resl.sd ** 2).T + var_[label_mask] = (resl.sd ** 2).T elif contrast_type == 'F': from scipy.linalg import sqrtm effect_ = np.zeros((dim, labels.size)) - var_ = np.zeros((1, 1, labels.size)) + var_ = np.zeros(labels.size) for label_ in regression_result: label_mask = labels == label_ reg = regression_result[label_] @@ -75,7 +75,7 @@ def compute_contrast(labels, regression_result, con_val, contrast_type=None): wcbeta = np.dot(sqrtm(invcov), cbeta) rss = reg.dispersion effect_[:, label_mask] = wcbeta - var_[:, :, label_mask] = rss + var_[label_mask] = rss dof_ = regression_result[label_].df_resid return Contrast(effect=effect_, variance=var_, dim=dim, dof=dof_, @@ -121,7 +121,7 @@ def __init__(self, effect, variance, dim=None, dof=DEF_DOFMAX, contrast_type='t' effect : array of shape (contrast_dim, n_voxels) the effects related to the contrast - variance : array of shape (contrast_dim, contrast_dim, n_voxels) + variance : array of shape (n_voxels) the associated variance estimate dim: int or None, @@ -133,12 +133,10 @@ def __init__(self, effect, variance, dim=None, dof=DEF_DOFMAX, contrast_type='t' contrast_type: {'t', 'F'} specification of the contrast type """ - if variance.ndim != 3: - raise ValueError('Variance array should have 3 dimensions') + if variance.ndim != 1: + raise ValueError('Variance array should have 1 dimension') if effect.ndim != 2: raise ValueError('Effect array should have 2 dimensions') - if variance.shape[0] != variance.shape[1]: - raise ValueError('Inconsistent shape for the variance estimate') self.effect = effect self.variance = variance @@ -165,7 +163,7 @@ def effect_size(self): def effect_variance(self): """Make access to summary statistics more straightforward when computing contrasts""" - return self.variance[0, 0, :] + return self.variance def stat(self, baseline=0.0): """ Return the decision statistic associated with the test of the From 6bf739e5c9fa22713078ab67dbb3eee0e950d30c Mon Sep 17 00:00:00 2001 From: chrplr Date: Fri, 20 Jul 2018 13:09:29 +0200 Subject: [PATCH 024/210] corrected wrong voxel selection in spm_plot_auditory --- examples/01_tutorials/plot_spm_auditory.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/examples/01_tutorials/plot_spm_auditory.py b/examples/01_tutorials/plot_spm_auditory.py index 678115a3..a8a1a361 100644 --- a/examples/01_tutorials/plot_spm_auditory.py +++ b/examples/01_tutorials/plot_spm_auditory.py @@ -39,6 +39,7 @@ """ +import matplotlib.pyplot as plt ############################################################################### # Retrieving the data @@ -126,7 +127,8 @@ ############################################################################### # It is now time to create a ``FirstLevelModel`` object -# and fit it to the 4D dataset: +# and fit it to the 4D dataset (Fitting means that the coefficients of the +# model are estimated to best approximate data) from nistats.first_level_model import FirstLevelModel @@ -142,13 +144,13 @@ from nistats.reporting import plot_design_matrix design_matrix = fmri_glm.design_matrices_[0] plot_design_matrix(design_matrix) - +plt.show() ############################################################################### # The first column contains the expected reponse profile of regions which are # sensitive to the auditory stimulation. -import matplotlib.pyplot as plt + plt.plot(design_matrix['active']) plt.xlabel('scan') plt.title('Expected Auditory Response') @@ -202,7 +204,7 @@ plot_stat_map(z_map, bg_img=mean_img, threshold=3.0, display_mode='z', cut_coords=3, black_bg=True, title='Active minus Rest (Z>3)') - +plt.show() ############################################################################### # We can use ``nibabel.save`` to save the effect and zscore maps to the disk @@ -218,18 +220,18 @@ nibabel.save(eff_map, join('results', 'active_vs_rest_eff_map.nii')) ############################################################################### -# Extract the signal from a voxels -# -------------------------------- +# Extract the signal from a voxel +# ------------------------------- # # We search for the voxel with the larger z-score and plot the signal -# (warning: double dipping!) +# (warning: this is "double dipping") # Find the coordinates of the peak from nibabel.affines import apply_affine values = z_map.get_data() -coord_peaks = np.dstack(np.unravel_index(np.argsort(values.ravel()), +coord_peaks = np.dstack(np.unravel_index(np.argsort(-values.ravel()), values.shape))[0, 0, :] coord_mm = apply_affine(z_map.affine, coord_peaks) From 8615a3cdf3b3aac4a433595f92d29cf924f69cc1 Mon Sep 17 00:00:00 2001 From: chrplr Date: Wed, 25 Jul 2018 18:31:59 +0200 Subject: [PATCH 025/210] expanding intro doc --- doc/images/example-spmZ_map.png | Bin 0 -> 33427 bytes doc/images/spm_iHRF.png | Bin 0 -> 20733 bytes doc/images/stimulation-time-diagram.png | Bin 0 -> 10011 bytes doc/images/stimulation-time-diagram.svg | 212 ++++++++++++++++++ .../time-course-and-model-fit-in-a-voxel.png | Bin 0 -> 49013 bytes doc/introduction.rst | 72 +++++- 6 files changed, 281 insertions(+), 3 deletions(-) create mode 100644 doc/images/example-spmZ_map.png create mode 100644 doc/images/spm_iHRF.png create mode 100644 doc/images/stimulation-time-diagram.png create mode 100644 doc/images/stimulation-time-diagram.svg create mode 100644 doc/images/time-course-and-model-fit-in-a-voxel.png diff --git a/doc/images/example-spmZ_map.png b/doc/images/example-spmZ_map.png new file mode 100644 index 0000000000000000000000000000000000000000..9d63a93959523a367c048f6e53a7006061f4723e GIT binary patch literal 33427 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 literal 0 HcmV?d00001 diff --git a/doc/images/spm_iHRF.png b/doc/images/spm_iHRF.png new file mode 100644 index 0000000000000000000000000000000000000000..50663ec29bd2ad73e8ffcf7bd78fb639e1ceda5f GIT binary patch literal 20733 zcmeFZby!x>w=TK}0g*HSl~h7RLTQi^1U^zpX%K0V?)nTs5kWyjNs;dER0IS>Is|Fy zl5V(T`HTJB^W1ax-sj%)-}TW4`PTa8nsdxK$9Ts(-nm}fQIR7hzCes12q{KhMh!u5 zu?T`QMnnLg@OS?D4L|UmZecWt;Ey-agXi%3Nqc!6Cj=okLI2^TNu*lAhr-Uc?>Vd6 znLE3gIGQ20CeHQ`?VKN4-Dh+)b9Az@d&GEy>pBOTv$6 zCIlBj21sxy5TwqW2n!zxNF(?Nl6M}L5katR6bRh?FaRM!5M$o||11Axi?KG&<6inO zx}vSD{7Owt?Ey_d?-bb6ghyH3*4DPAtu3P7q`ig*cZ{f~r{{`<#OVADm;tl2jEt_| z&EeTx^{4UcDCIGjH)nT{`ah!E~)lhK|Y4RoF;8-H|{ zE(-sFn~Y8WznP``SzJijSwb7VVvl31Ue5|XUeVj7KicjZ>OSc;!$BzS5@DYcjfmI! zZmbY)uuo;R}|?YXn~JhZ_y)->_y)B!A~Uex938j;x1U?!+gR)ma#6fn!aO%L zeQs~Ft{4I1oYI{5abQot*Q0sy^%)pDZymXnW^~@8^^$h+uDl6n^U_U zYtdb$$Nl0i+k1`W;5;s!!l5u(2xaJ-y<-sl>tw!XRc+*ocg=C~C1rVsxq%O4hxZp& z9Wp2o>^B)i=HA-Ave-I<95ZIyz$oqegZ>jksVt3io!sBwF&}QU%k}n*)2&~?L7r>i zOFm0(Bb;|Gt1)0-y}nobPKmD1X}OG5`*yfmZ(nz+pYOuA0V*Oy68|!C>Dr-r69&^I zmV83tRVds?OC~*9?edO!&P|k~SBV=;Cw&RIw5TEAI$wSH)eVxl9Gg`AE+@OH`%i*c zlQS~=Q%AP~ZMyT*}%x zOc7YlflkjXaAj|E&z{m&R?87Jd_`wyyO-w5*NTTQUVWyFcyIl*(7bmqZD+^*95waA zu-j<0@&2oef*EOP@?4RMw{PF}oQlxC6}q{#HSaaLAD2@xf0c!Wg1dmd#fYx{6GRn@5i< zrYzqPB9aM$A^Dz)p+(9G1((n|5! z-%8KQQsU6g>G=^W%7TfFjU6ns2&MNsa9H|TD92rUP#}J|p+4-;M2DB-v_0%*voccc zIr&O(qw%7hCHx;cc5!|C<@(UZ-Poi$kJ=oX88ah`-r`+H5fUH!0-gA{M;Ei#5ac;= zfK^mV@9np5-&%fta>8@DeCF16Tb!7l+lWVMUygxj+97nQXK*m1-^fqQc{ICpBx7U4 zxq4^frF2bWjzOjKuuK0Oc!H=KW zT!6_HPEJl|($XP1V!yB=%g#&X!^@`(2~B6})#)`$o!PJdNWYl-o@ZhqyF$PyY-6zI z!shP5HM;W40Lob#~Co#`$dhJY_g}Key6#X|Ur7 zW$94%!NI|LU!dalR;mRvMzHOI$4dO!dn9YgHjXTqT2C`$K((OiJY~yHJr%WCMU6L}6UI?o*3#B@dPFASUkx8eJau&%632)0jVRh4($ z;ndWWZk7A$>vncdPP!i7_31XeLBjgc@%q4GvsNlIb91tafTqdL{gqmV3cHyfv1{%( zXM7AITazK2;0OkXWXoOnEtstHlRfIU3Qh6Ck9Jjc*toHV@g?++;CEbvq8U8AWEj2g zj!8sBgw^;L|Ervwz3c4~xh*XUVt?xJ!jim}%jG;gJQB`*CKuay{f^5Z!)2k*sv+o{ z=Vq@q+6~*`3eB*d{{GZl-_1;PBOUEDNwmzY`+Jh`-nem=nmYAop~XUIXpAx*>aFD$4 zJR)qSR<@d{X=0rB8m70e&wBY+`CyS%I59CXGX{F@DD=Zsi_5U;9(VOd7`@lxb+MzJ z$tZE33W0|M6uO;W^F8du{HYB)o|22x=-%~iqH~-5 zU=eqIq4Xuw!v&)xhr{5kyR};d!aw(VOU?{B{wdn?_H7X2T-hN&>eNi~$&HilGbD66 zWR$D-=Xxos1SXKz)+%9gIuG42@$I?(5`&-K{V=TF*u1UF$GJYCii^l_*3{M;lQHJ4 z--AbI+}utJl)wZzh-~a~;7QloEi-t&WI&L`X9!X2(oK4rBhC-+uAArPd#f-ak4F4- zojvi8#gKnooW;@C5OS3+6#O3ZapzuOhF(QcZ2)N%4zeYSJWEz7bzOL1no1j>ag*QY zyCK_n%;b~+F9`{fCydK@t$5}^A@f{Ij7D-}Q^=eRXMFf&%%HdNAA&IefhTb&-et^6 z9GV`zExPWP4Lg~!{xw-|ae7U$*LNdw1Z#gA9&q(4o)~sLHF)@s-5v7+MJK1>up~F3 zbNGLO$Gw#Pnf}1kn_-Qqgm=Q;8Bz{d?hV&lG)`?I2$3AV9h>O&_q&DFiuXq}R8)LRcEQ zgnYTNpMhIn+y6VX{ZB4)LxYfph!_va0a<+=pfmI`gEk@VSg*&jgU|5A{TZB^b#{Qa z#;Wl6Ppu2x<9h;_u3rvhSjBz8XCpoZI!j_clY4^zA@IOL4tY76zUnOsuI!?{{5iP> zuRkQQpMNpG9zEz=A1+eoehIn%%V$vH zctznQp0VzI^r;c>)V>E+oCp%khRfJEoM23DTvCj4CrsgKecj(SiJ;Al>QvR#s7mG& z(}=EZXHlIi&oz9Uzbj|*s?dSx^@ik)B%zm?Zxo1m&1kyvdFe9j%;;e#^e`0dcmi!i z@8&W0w^*9YPngFZSQ0!~)lZP;4lc7XoW@^nV@!$k#IBCZx>X+L-AZTk6p7xy9nL3kcFk58p{isFovLGCUu^=X8Gu zzwRVE*Ew^RX1-*xhz(p^n7-k zcd4&p?n7NWtNmSI^c&L!(F)iWT2o&}&SWYrSd>%J#DR#408PVm^Z3QHPh`#?y9JU7Ib z41Q^9fh%S`E^^FmDJ8{-XxU#PLKuT6q2XI%*fU@3PW&mSQaYK1KP@i=;qNkX|NHJi zii!XMu5sBm@?(V@E7$IvmfUUQzz?g16jsZx^l6pNJ*D#dv@ME?c@;6ITI7RH`S%26 zQ2p)mL+JA~^cl?LC`9ryv%G9qeQyS*fC3+(NCrzdeTLchfnBqBQxe@XoiC8**Qxb! zXn%MiaU3+rwpQ`CrhpCvdqoEM`d*a&0xsFgxjLyrN42+SXEeN2<6-PN=-3lO&@Y}$ z#m2PG@i;3V2XuI;+yLMATpUK79ug=1p_bNI$N+M_o4Aa&RX z4ZS1`7jNMqpU&e@n9jRPKS0g@h{z|HGyw;>`}f|cV`>u|`?`RRhlF^@^BZWxlcbM1 zjfpQm09SD9;w{wK{^RqO6cvn`lNex0z#1u#^U`vKFWUnP5(aiSy}>f zCYZS>Ft7R5Xk5KIrRLzO6O2*`5p|jykk@}Agi%aCFttBn|9cDhvA}>Ut~SaAYo(zV zk^m93u}&WRm=BA@y~pG1o;YBR$6$^##8}EQ+#UVThLS3W8%sld|W|ook+QOb48wTea4LZ8GxRHRCj@(bv(@xj`cBwY^N|{3`lU z1t!3Rqzn+PLE>J`Vn+Lnwu@6_(zjoDfAl|lV7>k>%+n^6h~UG4kG+}N3!QQdnzwKJ zKPnq17% z3~rD5)mq!wNSC63BXMtJF(V~e&&OBY;X0XF>Q_C3AJ4x_Sg_<-6)^E3jB5?|46w{% zha24$+aq2sqdwc1u)bWQT)|Oq^>(rCn15jPz0C!eR?bq(wkZCvBz~K|*`?CaKKO8R z;w862SW8O_GloqirfkfHjg|E(8=J-1QPQNs`r6&OlYbZd`42zC)y~d1rSI>c zwYCqua-+LH+7;n4e_f@mSux`4O-V%3Ij@Q^FCjrnJYMzZ&!2e9 z?LU5K85j(|Zcoq5tW4UPSuf7@QTF%uZ)tAsY+~@sf`M3~rsU>kDDY8L4fXW&yj5FT zQexP(@Duar>}z>L9Pgan-MNgkhj(wA6)wlbTet|rmVZ8l!}E=Yfb2|EitudBn<9o5 zfOiag9)QfEgq>2Txb)M+ytdH1=jmXsL8X@J&DF0g)d2?nTS)L9SfA@DNtl&2Uz3P6eG$SK} zvf7aEWq=sc&R^@t@&1rMdwzNIuTQ>g>VX8me5vl>u=>%Ye{Ic?)3D0@+sxZ{@5roc z0sXC)FytKWZ)ZYE5YDQ6Va?%RXqm)3%KQoH?;VE#CVQdxzKi@bJV$xzKqOhf>%4IRVMy^216Z!D&#LLKq0C4AXC?vglRZm0zwTTM`LG%S$ zy$}|w-V9CMTHk6ED7Qrms;*S6hX~jXCOvtAi%~N(OGfv^5;eDBR-~aPruJZp+XZ0! zf_Cj*1VH7FVPRo+qWcZKl-Fn4`>AVpZezL(+=i2(Ckv?(hv`X@?UV6G8*J$2E;MiJ z0_2=#*80v0)~usQ?BvdJVtez^NNr4ydqb0K`>2_{^epi-4lZ&j#-yNd$;`&qHezV$ zUDKqA&<<_xa(16I_foHm|M|T`L4|BCi=Ky~T^}p`YScZfPI|7qsm-gP1u|@6UsTBx zp)~kpd+Pni%=PXEG-yUUuUDCkhS*kFA({b7W+J*AUj%hv}8O$P61gDcs-ZT^mUdb*aUyZh&C#107O#|<1`?~S?liM z8NY4Ad7A__4of0e_4vJB=u3=He1LCh0(_d^{1D_o(}H)|>EI6pj}n>p=eT>kCF; zs8GWR4-aOG*QVb~O<25w`r(8#QW?4rq8<{FBG0d(-^fU*!+mWz43}<~rw2KS^z@8t znjP;GAdG4-NbAKsg!$XKNsYUYGX7!mKrlH(4y(>;g!u{|$#-8E1(7s7DZiebafX-+ zKm8*i*xv*!$)7-9N8*KFcS0Rk?=x-MYFcH#1&Sp|0G%aHYbb8S<2vRv&z)>h2vN^% z?>UOnLXB%Y0AD@lBVKE*SHberf@pw;WDBsa3^OjH?CoJ;FGNM@#WLy^{D|3}0AoiE zja?EtZYKi}VdE>#fvyX*lax1cxwzZt>uaU7m0eqTlIGuW^_IH)#ZL9Npuc%Gh|-JD z-g9TvnK52-C*TQ0=o4I8e=-?YnxFx<3k+~IV_zE?$|k%sAwz*k{$_$1=@}>^YjZHw8uXppcpTutc#AUT?{Q7{ULAZB(bI#r;8s&rS?}DlCDO%);R6kDa zSRBBcg~bxEWc&}>l=2i8(hN56&g!19&T;^Vh+>m3V5Q6EGrN9L>uY%Hh|WoX8~A85 zSCEP&S_HDQz=fgSzTv;{OrHjBrlQ6Dp;&5%#Jry90uKl4Wt=_q@8hM=?-IBI;n5jFsnS3WYVy1M#bYzDil6s*;I1F*L+oX4(9wTY3;_#3t3ue-40P!eK^Y?W z+s|FUlQ&4}VUU8M0L8IHfV-dP;ktk9c)^ISNFw00gXPQ1}uG|+KKCs0tsuo5n$(8gXQ6L*?zP3dzxKk!7?Fe&Yob7t_rS~W^O7+Q0Ri&t zGA<+6&`&Ys{Lx{Shx$Dt+`nVj7tliDjqEwz`)K}a(HJwroflz1cGF+a{&Ts=`&6Ry z#e+b%ULo$jz&K>G5ehytM?Fdv1>*1%mLm>~hz~_<&j4w1DZ1sVk}f?KTPDIvPoitI z4DC{nC-|n5`6|gQjk@~E?!-CxM@2Qtw*Pc2I{1?M?+VwCC(LL9U__}e*jJ-s;uyQ% zir<{dM=g4b2wQgTA+Fd>GU8sag6e5B^PB;I@I*@99|XO+V@&L9o4OtNe#v$Z3T8ns{IOC;d`pD{@HZ zu{@LlIDdXx#A}+EBuq?BYU=A}xdB9bvLyKE7fOki>vI7Dor};mJpX5>mAf`;sx^{> zgF`XE1SM;q=5%&H{s%=GO5yG*Jz0Nfe&ka_Fc1eixnVN50=`+1Mn+y zoKjf&RnFU!l9(v1`0iR~sNccldd>2DZ{}tTNA7HLq+u5zg5HsloFThf1Mzsfo>5ai zE6eScp_xb%`Jd^lT9IeAa$=*HFnd_2vJssLI8VUat*D?-Wc(Rd4CwKMee?FXY|)h} zb({VipaLPvTie?9F67qISF8ZHrs?TfVd%ZufMcr~Bb4yv%Pma}4a<=#kA!_O`cPJ- z_a8?OrUiA2ts|JzQ&MDicXv_t>>am}Z)b&`p5Bye<t!=hAFqP|uO;e8S{QNF;Z6O1Af4Y?8UuSK6y$5xt;@U=26mCbq%&$N&p8qE> z=z3&$_+{Y1y;iEVM~}8jBR+gc4-O{rnGqfB)YLXejPqVPH&XfD?JdIJA z5vpGQ);+wSZW^c^#wX0#IbnUNwHPIjp1sH$AmmZ|>A;YXXfe3C>*ubAaZvF&__}si z$K8~=QdMAqJANo7I7w*9chUT48L7MEdT?}wBAG1V>{+3R!Ll zzwRN|5V>HK#n6I?J#b;r>-XmF z6i?EkMgW$(|I3jFcBSw%bcGU^F(A9b@5sp2y6S$cw=I6a>&=b{F58A?Cc~P@DvjWQN{;=6&`-R&A5St=!b|J z0ucoDRYH_Op#k2ML@$rf{)QMf@e<%hKp=HOFrIri4XUjd=E7yj$1HVvYAGJDp$Kj= z7CKuO7wEsc?nJn!e3c#zt-@;fRN>`gZMm%w#@dZ1 zG*6{W{)ngzZGDFku05&tgSA+G=00Z*>mEM!6L04Xe{>2bfd_pl;&v2oLPMo}#)q~1 z3}5!K{urIyc>oW=Vz8R#HF(#L8wbPA>x3n3hw$CgKZ(?l!J15)e|Mz(@x!s8c~1O! zBCH-d2@Vi!Z~F&ey9e++v#v$GV-IAa6YEaURgFYp#3x!D3hD;oI@9)8ip)&5R7_Y2 zeF`;LsswG$dKw9Nxqzk)G;Dwe?&P-TR`fl>sELbVP3ISH*2#WIu>!P?wS+X{M6X8$ zRL=qR=`?d?w!W}lPfcQH>PrvUqW5vEXhUsu7iR1f@sHzH<(HB5pXsrt&F3b2T>Kxa z|4n^p6yXsa<9UC@19;QDp_=1$d?*DV-Sja@atyc6z4i zurv0(2z9Y;4up@ej5WJksl{ z7xC(AJ?2gZnZQ;2z{jIUMTWi-f7>S!Q6^zH69^csNMJ%w!#+;G zN`rhjwn#6d352yMbn!i!$t3zC&zUqgS8%=&8!wztJ%K8YWTAE;O-zP!2FZ4t%&NPI z#!>6}KmV8u$gY@ijd$MvR_TZkX1}347ptm6N`lZp0gGU6ZO=I+TQ2Nv{O9ZT<73== zrBB%<+z0>KAB#MfdcuZH`r7m@mIAV$*GYOo1uPglKS$YrW(zVR(8S4Q93g-m0VenK zNtx%OnysSVPpAQj>`H4>fG(wH;Cv{4AY>9`feG#HWE;5e7}5&S<(GtBpu#>&H9M@Y z(0gCXL9eiRxA<>LP6^9jY8Hn?bv0jQc)(A< zeiugzurrs@0DhfCypI;paY3P>+71r+BR<>1r%Fc491??qf_^)eYiMXBX&ZW&)lH^Y z%@a!OJa%+)ig^9{BTA?4Z?7Llp$q3U0K;_V1%LWgzPluj>s^Z@StJT)xPn4%xTVLP&<?{baYTu_<@g>h7-bd~s@LuhuMp^S zu&~^Q!h3?AqvP89+EIJ0-m^k@?rPRm6ZgMNOg?r>0_9Cb`uVUV!RRyaSpT}w9ak7MTrBCtDXJ@CVg@U<$29;l6DONO7iJ`mcVFiY zLGBV47uT@@;)iI&V1@Fl2zh1; z*;L-AczjmB63~5g-?nBm+Yar150H`BK-#NH9L}k3zBD?x_3G6t>6+=mVjD;Rq)R~v zVNzWxf0_i_%{t(95G7_0&*xoT^~nk_V)Iq#N4I8=+qD6 z!_UxMs(fps(Dmu|OJckvZ|mh?}bZiVq#btN$|xRvWGC@a(1zhs3? zvh%^1oDqj7W>`eRRY!yQAlK?4IcgW-1n zRy*JtS^p<$hksDGE56&6^Yimvg0;KK{DOj+Nl7x%f_7R83PDMdb|V3#7dO71QB4C) z&508yDvpo#H-}ez-MmM=X--0nR#d$6mu#}XZd7(v&MQ-QV^6La^n`UR*OgQvY9GQ< z1a%f@P3~T&tuoP0t|?u;Jj}zZSX*BDdqNePoDn7BQm;A!$>NCLhPfF_Wfs>-EL>S&|qqum2;Tq zimUlitofW7WODd3Ko4AQTGF0r;YlJ_5y-4aZbk70d5+~S{=x541G zW+a}FkI$5p3j@t7a%U*x@}}+I=!W1+$^$xMsIPs)D#os&2-=7jhG3UJ6bO{`8!7E6 zmqNy@$9(wj+o&RS4SzrF{6;(`?+U`j)AwUVItS8Wx1wzkIYC?ZPra9q{2kP3>*U0Y zxwsuufCd;K-fn>!AS3FDUL6H;xz~tF6{L3#My~)+qQox|VCA%ebah{Kh`rXgh-aE* zGrFukw74{77FVk3k;VOmLNw5dT>AzCjvx~z;4>A^o0f5&jj0ol#c%|d%>fwM`{GN^ z%KI=PKVshBcVMAO&%H)Of>-NQCItld-^Xaf4S(k9ra$u*{>0)L+loZmeO0iKw98Q?hp@Ijoh_=5& zm&TWj7VZ6&9w%Nq0mb5W`JxKi;3qvh_lA94#vZ(;9?ku02*BZkzs5ltG zM+phO8elRDGD!I_-)-@iP+-TSL~NIh4Pf*wCMWERc3Ym@1Op#E7x?PFV8{cAcKw@Gl1!Pwg64gie(Z6EZje`UWTnpoIrV&nLZ8#v})k zw@-v6X>a$XmDi*6E7o2LDawCZoE??~f>$9tA%%0)Ntu>v86bh#J0yH~66TZ<8v0yu zgb2OZ_3LcmAvx@CP-nr^{&Y|imq0s=FR3T0(Rv^uT4rjP({s|qpkr;g~WT-jY$?+|)xzj%k z4~HHJ1P~yVupsW(@09XzVA#=m_|w(1lg_gvERV~v5~H3muM%+TQeetAOwO$zFFu?C zIVRiJd5}RF9+&l@hN1#1cf_@rW4fNO&w)$fU~X)9782++EfMlfm75V6Aj9b}KkyAl zhz>WyZn0F}cTUSu{^y25v(drtZ+Mri7nap;WvOecOyE;gIiiP6b(`Rhi)HF#8$;E*TZciuo$ir&0?Le7xkZP4m%$ZLgWL zxsNqqo*T|V26h^!gZ1bDW9&ZzbD$9XGQaG?v&4cf+^J-iw8rjiqlaSVKzCuU%OFKB z#vF{|#hI^le`^* z|Ks>@?cnCiOsS+7FCAtBIi?lD3LSVT1O$jm@nU7s_ zANE=fZbIGX8;ISPCS#0|=LB$P??`tlwVReg*s_tXr33%-2cfa+$5F8$2emD0`)nkWy zVH%_gz?BF=ZuN^Jc*FJSXXM@L6z(4C}ZXLI&UFITSgjZ}MW0-m3JB{KQ(ybz0<+d4+o2Ms_(G`9_$=s+Ua%isn`$lgJ@mwMtxh&+YHO!Lmgi3o=|aV7 zRcjN=+7dfs-A5WnZwo9%|Z{pl+_ zriG4|@4;R@8=!vfA$S*LExbrSMl2 ziM_ugNA$3fPZ{baaBL{sEMHx%taI&EsleLK6Z#G0 zGUUAX`s`PQGyZ)=u(zu>6Xd`^eq|(gXTHlZI zIwwu(zy?>v8`-t9q#aZO@5hogGzy)a;lq^>r3<3_Qz-3lJ$|?P*ddj-<1+=VA?YxQ zbjXo}X^aH}`O*1Fq5ZPUJ=YVYmPjt!$0yWDP|-CC`(6Fxp7*L=XumQ+GP-d4ykoy#>YH9tt@|7JlTiW3AEz)5S>V?jnnWTfE)uzXj-ND*1+zW#b!`yZ zkvk_FI|Y*F|JP?8)W(cRP+kqP|Ktz67S_|>KcJkz+xPF&DpzXs)Qlrm(E~kjIKxO{ z|7SZh#^`vj-v!K~SApO^mua2(J!@wdoy}k?l6Jg3N}k5Zdl%r2v#JOW)nJ7eitzb4 zAM3d&mRjppXe`XBDJzx&r8rgj%s4WRCKKvbQ2t=&ysl;S!a+gtxhCkw;YixdF9!GT zK3^S{;QaU5iBvaInaKS|S9tlY(TV&VNN*%txgF0i$@6wHL|etWiZ!KzkLhRfN%omv zVUzq!4aqRnhU<&ZuTWF%BeGYwC~*nqH+Nl*YkfZg>n@wVdVTQEE48+`ug8Ks-ar63 z+k}D?c<#@fD_xFV|9H2p2&kqwW2wbGSOK*HrQPDfc14g=3RGrMZ>bpeggJgTxL$hw zb-Ppsp`{d;tTP6%+Txq9&JgoZORGTfcuO)p#he8k=a)M+Zn->4m4q|qSW{O1$)$o*epnb z`E757ou2%1fBN(#+R;%s89I)KA7kXBSGs%@87rbTnzN`8>FaiU3982rU36#&<9-+} ziS>S}j^G^B>&@~>y=g7dPFr17gb&gi>#ZaWgEw?U-s**?r-DrE)Pw2%)z7j#a7BBA z?QXN(-AhrR7P4F6?97UJ`%;3EG7(bZ&x2Y^*~A1jRN#7PdVb9D@R{B!nEsuTam79Q zZAQ^op;H+F!>fnN|1f3)f64Z&jl^iWw2LhqSnRC*Rd|P``$jg`smOGI(LpU#C~B@$ z;j>aTE3EJ$w0m-spZD(iS~nkr(f;45R?tzEYqti*_M;J@OWs)QIJ$;LjQl935r$UukY=QOVrc zJs5P$xV=LEr~@!8#N``lfp&Y(lJ1OEx>^UYrm|7|qU_u`A`1_fLgR& zsyXdM6g)S8BHG(p*`6D& zCk;!Awy;=`Gm6Vx>~Vj|@$%ttwK!^fy-^X;6)uA2uiEQ(Fgi{`w&=gY_mHrB&1X5a_;SS#ckKBe99-FUSear4tI|uNQ*W3#ugUk1`lQQitEv>1 z75sr+m7e|CWH^x&U?$z49cH(xoOnu_K7*TLif7c`Hxc6hd&SOzZtrlddU7wYEgp`` zvSIdGPlRtSnQ4R5_&hP61?P5qOA<*vSL72*=xjTh8-L)0vQ^$MveY$Ci-b94o>s@5 z`MfQNV=uFV;Fu;t(hy&@`ob_h?cLe;g(Yi)&3n_zybsUdi&8Kw)gcJQE%dzro_iZd zFIa6s0Skxr2-_x|L`d4&RZq1|RE;MCCdlQ& zURQ7VUM7owg>}Ku#JsFF$iDV7bc>cOKL*;R9qyQ$rCP2AD{+2^OAfO-uka$YI8B$a z;NK(O8SpK-dpxNcZ{3ui(&5u^Vs1~hA$-$6lZ~Ap_vsy{Ky6S<*z`BEE{TjJg=UD& zZ;~FguY!mWZLop&#)l@t;7k9WRwI-#Zru)97?sE(-3)>AAQk0T^jd=oHp@BK0?cSO zwsto$2GQafTeFGtR6h2fbU>;|SR4(W26&^#%I{MxtQ1)HS8+@q|7*Ni4i&|E+$-sQ zjbf?JzhfBBP4a;1IcI^G*V(`%fJzC&&~G}e)U5#`5V`pw7dS^9XsrjuH@QITA&^55 z7RQS-Vft0tU?HBbcw`;0#w&E6l!p9YE83W|n-9G^*H1qcKt16zDfYZO6D=`kLCKeO z{!BQ(>HVM-Rr#*3J??O1PG4&PD#X15or&|)TyWrZPQxiuh2L%7(kBq&PWmqIP(D7C z_FXm~l;*TJEkqsg!CKU2mCM3;_ircm$FV9w8XSxDKV4NXr@@TvMe50pXAdF^)btgC zU0Tj9+==NiwJ1ujTM2PK>yEnA!qcpD#=hW3zGR7ZQN+{MN4 zLbaD9&A(mJACxgT?4?H|Vds&_&oiWS)|K+lE8o)uCSa^q;h@ePL)}knT@(C1_X|=~ ziGB&0;St$+`zcNe3KJlWI*cD#EPuv4-gevmTXD4*^y(Q{*3`~b1!~W857+NP*u?#q zpqD2%h*%p%BHqxZZhRFoV{T+;5f3n(M`ZY*SUm()?fK&(x8v(~`~7I^;t7Xw)}nVD zV)RMQD*3uk%3U!3A}YC9QPr({6i+m$cs$k8d9qTPdbqn^-U3|F{`Nm`yEV zrYN)awYBjKB_(Au38CaA`14^Lk(ZH?VdkYpUOa#P{6PR7BV2v+-5z0vt6}HKuy9r8 zMm^4JxGHZhgq;2_eEHX97i_r_5e`B_OZ!&nBz)sgFI=LO%`c>tlCbXB*wT^+`PggC zqH+t`KF&=@U@EJuRHTzWP0Td2x-Aj){(W7sP5;>ZJhA^R2RQ~WdwUAqY$`aIZtvt& znHVY-na?$yF93geO%1-A+I}zeA0%Sp;p4-9C5tC2>QQqsL=HKI|B2$`2@g;xZ`{}S@P4uYlmAksF>{qV9xr>{Kwzl@p-rmG&6N8+h;>%`nQ&ZCcIJ90>T|K_I z7~DkfMPguJaGIqg#!o#OnzQmLg!9TTPV4=6b6GAAbYznaag!4h_S4_bPr8O zJ}@_*fGk1U!$bJ!U|p4&g~jB-gRf<(uC4;WJKa%Jdjh9KNXW@ep%%$QLvot6cK=A( z7GV?<8&0wddijz?;h``N4vvuh7y`$hpTDH$zI6RMCDh0Tk8ZH{$K|s>$!h6JR=h1M z8_-;VrXW>&^O>hv3drCz);W<_m3bO1LhKa|4qjie>(`ljtc#%~ti$l^Y?{5B8}&-{ zcHCJ?mg~h_w}{?EMO}rnP~3MfEp+zxvmwu+_e{?y`g4t_6_VB9)dn=%bIZ$AuCA^V z(RMX&uBI@j^vM!2fl#*rw7Azyt}cwv&9RFg{E^-YelUw;jXrJU{Vru_$CH} z0a`2KO2lTF_wEh83#3n;JjsJODk>>aplhDjul76#=j&O;evge|;k^=D6j(TzQs3WC zosf_quc|6%WE9`?=Gt=OtBVN2&CQKS8W_+uHZ>`G$|iO)Ub=M4RgAY-AUuRll)%o; z?pa{qWj3~^>ChBaGaee$#&6zGer;&Tw|I!pJ#Ks3_xR9_hJ;{eclSfq_WHWCon5X> zdR9H~N`GL5UFPFETYI>55%|GS5Ev(>r=NtgV!1u5S&a|kzxPNlEiI|Z6kK3uXLpAz zkb;KhIV6Yo+vCLKm6T+lTeP&aNg)=Lw}=)8^6@ydvLt0>@UgX?vdLx^ssD7SM%@7xUp*7`&dK{R%gCBu};(A0Hnh7Z+J#Vxql+gOrZW`7d9- z+%Yn0kIToQMY3PtO5oYmCAw}K`zt9aiN;P&R`wFyeTwQ@og9PDGk9Ty{QK`B`GF}z zRUU<_uJHW(rlyq8QT0?6lllPC)2xLtaK!9jt(7}1J^h1=QTb+Y`Cw9N>P0&_85u@q zX8ip8{2ed_B^{mZ+DC+z}81_D;YXP9vpdWywfN zVjZe=vjYrso9gPY%}0*&=bef_V4R)#-YO(t+XJhaz{?7rzj}3BN(u+|;E3gyQVk2L)oRtON?lT}c)|bao63Sgwol>e-ri_4GVjU3G|F ziT(gySU`YvAugX~3q;ksYJneXfgm-3X@4)_Cr(R82SlUY$AW^>a-sD8fq{WNtIXly z;dq3EghWjCb6qSC%*@7NpI+hOdS_q|5E$6`>sRo|h#|Z+!SrW=nZ2Xq6T@8i8X*Y* z9=z)y4=fHVbn#dDnc?AK_v6ED+r8>XrI|0QtHnSVG!F6O>1XWTo@Y{WvWd4hExdC@ zz##JbcO_a;R}wbWxM#A7pa1;ff#;am*^wu!odoZ!U+GE;_jSxUmpM!l+_-TAqEHYV zd_3doTH+OBTr#L%?sx)juy=KZ*^slf@SsH!2>Tqx|R*7a05*#q8qZSCFSa zRTbaG2ek&#sZ-&$?E`0Di2Ep&t^#QtR97dNpq4J<>|E5$a`Lw|C}_~xxDufN-u^2n z5@giX!+Y|X@9OBVBT9Pk4ikS0KLcWOQ1IW%);a|S%LkEtC%iqx*78)Dfx)AZDs~X_ zKMM^Fy>m5UqGo^jOnQ3yC(z%|54a60@#8Rt(2H6Bnt`}+3C;qzdw85=V`IyAUD5^b z)Y;j|q?M!3UR71~Od<|ig%?b74;b9j+DRw5_Z)C^mTT_$VO?Vy2#IJIv#wEw4 z^Ln2sZV@Sx#h~zjnUg-1<$oV8pjacitFMp1|JGxeElgZ(a`Fl5;tSH!(uK=I9UWLK z76}dxzQ=f;vU3!IbZ1P$ohy$j9}ZWX16Uv?CI-RxHM6OqA^i^5+4)7DIZb)!FNeIa{Bb9Q!PD6&2LS85uDE9s0E4C3SsU8!3p4_+XB#yt$b0;mj~4EiDN=dDdA3L{Doa zyW;zcyzoo-cy4ty-9lgX)6`V9nAliKdis#=RMm^6aBpI5EdywW+{$!iWTuBNyC9b? zDSm5jm%DqH5@^RjN86s&J(XecvCR+CEY z1_yHr23F;Dg7MNGA>cyhQy{r(0=*VK;y&(Aod?bt-5j4ke@3vtuc3U}K(P%q*wFsZ zm=s9&P-l%$(9#A&Fo!K2O@|B(4pN*sV_J}tn22{#@R7`e2T7|ADN*1n1_uY1dlXqN zMV$kj8s^WU_>RVIe|Oge4so#7zmJb+s5w6JLL{A>cxC0}@;x`L!<#x5HlGIt-Pors zeKh<8f?9Q-KRzEkT2Y zr{nO~uTU{O7IT?DJHN2-3f|_WKX868P}(<4^zf{T(m<9|Sg)GrBfxFtD(&SOD}Fx4523~1_~St{zt1*vGqZc&PACapoO>+zfQw7;j$ONy)YaL6JIJ}z z)YLX49p&QT<6E?Co0y)So`do9PoDJf_HI6yU=Vjn)!JGbSk-dy@*X{KfB{%s zZvYNMKduD^;)T0+dqY=;MLyXoC zo|KTl0Ls_@EO&hSRt9WV7XDCH>V7N;?TjpMpu89ItX?g7S6-VD+Y|;M@O1TaS?83{ F1OPqv(nA0M literal 0 HcmV?d00001 diff --git a/doc/images/stimulation-time-diagram.png b/doc/images/stimulation-time-diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..f15bb84ae23582e95e6719e61526eeb220e23752 GIT binary patch literal 10011 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 + + + + + + image/svg+xml + + + + + + + + + + + + + sound Sound + Buttons duskDisplay Display + + + + + + + + Beep + tada + left + right + picture1 picture2 + probe + + diff --git a/doc/images/time-course-and-model-fit-in-a-voxel.png b/doc/images/time-course-and-model-fit-in-a-voxel.png new file mode 100644 index 0000000000000000000000000000000000000000..6e15589aadb4bd08f5edab2f2ab91f207e8e64d1 GIT binary patch literal 49013 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 Cr~e)R literal 0 HcmV?d00001 diff --git a/doc/introduction.rst b/doc/introduction.rst index 6a921b56..84e183e0 100644 --- a/doc/introduction.rst +++ b/doc/introduction.rst @@ -7,12 +7,78 @@ Introduction: nistats in a nutshell :depth: 1 -What is nistats: Bold-fMRI analysis +What is nistats? =========================================================================== -.. topic:: **Why use nistats?** +.. topic:: **What is nistats?** - Nistats is a python module to perform voxel-wise univariate analyses of functional magnetic resonance images (fMRI), using general linear models. It provides functions to create design matrices, at the subject and group levels, to estimate them from images series and to compute statistical maps (contrasts) + Nistats is a python module to perform voxel-wise analyses of functional magnetic resonance images (fMRI) using linear models. It provides functions to create design matrices, at the subject and group levels, to estimate them from images series and to compute statistical maps (contrasts). It allows to perform the same statistical analyses than SPM or FSL (but it does not provide tools for preprocessing stages (realignement, spatial normalization, etc.); for this, see nipype???). + + + +A primer on BOLD-fMRI data analysis +=================================== + +Functional magnetic resonance imaging (fMRI) is based on the fact that when local neural activity increases, increases in metabolism and blood flow lead to fluctuations of the relative concentrations of oxyhemoglobine (the red cells in the blood that carry oxygen) and deoxyhemoglobine (the same red cells after they have delivered the oxygen). Because oxy and deoxy hemoglobine have different magnetic properties (one is diamagnetic while the other is paramagnetic), they affect the local magnetic field in different ways. The signal picked up by the MRI scanner is sensitive to these modifications of the local magnetic field. To record cerebral activity, during functional sessions, the scanner is tuned to detect this "Blood Oxygen Level Dependent" (BOLD) contrast. + +Brain activity is measured in sessions that last several minutes, during which the participant performs some kind of cognitive task while the scanner acquires brain images, typically every 2 or 3 seconds (the time between two successive image acquisition is called the Time of Repetition, or TR). + +A cerebral MR image provides a 3D image of the brain that can be decomposed in voxels (the equivalent of pixels, but in 3 dimensions). + + INSERT HERE A PIC OF A BRAIN WITH A VOXEL GRID SUPERIMPOSED + +A typical step in the preprocessing of MR images, involves spatially morphing these images onto a standard template (e.g. the MNI152 template from the Montreal Neurological Institute). One this is done, the coordinates of a voxel are in the same space as the template and can be used to estimate its brain location using brain atlases. + +The series of images acquired during a functional session provide, in each voxel, a time series, sampled at the TR. + +INSERT HERE A SAMPLE OF A TIME SERIES IN A VOXEL (or several voxels) + +One way to analyze such times series consists in comparing them to a *model* buld from our knowledge of the events that occurred during the functional session. Events can correspond to actions of the participant (e.g. button presses), presentations of sensory stimui (e.g. sound, images), or hypothesized internal processes (e.g. memorization of a stimulus), ... + +.. INSERT HERE AN IMAGE OF a TIME DIAGRAM OF EVENTS DURING A RUN + +.. figure:: images/stimulation-time-diagram.png + + +One expects that a brain region involved in the processing of a certain type of event (e.g. the auditory cortex for sounds), would show a time course of activation that correlates with the time-diagram of these events. If the fMRI signal directly showed neural activity and did not contain any noise, we could just look at it in various voxel and detect those who conform to the time-diagrams. + +Yet, we know, from previous measurements, that the BOLD signal does not follow the exact time course of stimulus processing and the underlying neural activity. The BOLD response, reflecting changes in blood flow and concentrations in oxy-deoxy hemoglobin. In reality, it is sluggish and long-lasting. + +.. INSERT A FIG of the iHRF + +.. figure:: images/spm_iHRF.png + + +From the knowledge of the impulse haemodynamic response (FIG), we can build a predicted time course from the time-diagram of a type of event (The operation is known a a convolution. Remark: it assumes linearity of the BOLD response, a assumption that may be wrong in some scenarios). It is this predicted time course, also known as a *predictor*, that is compared to the actual fMRI signal. If the correlation between the predictor and the signal is higher than expected by chance, the voxel is said to exhibit a significant response to the event type. + +.. INSERT A FIG SHOWING SIGNAL AND PREDICTOR AND their CORRELATION + +.. figure:: images/time-course-and-model-fit-in-a-voxel.png + +Correlations are computed separately at each voxel and a correlation map can be produced. + +INSERT A CORRELATION (BRAIN) MAP HERE. With a certain Threshold. + +In most fMRI experiments, several predictors described the events. To find the effect specific to each predictor, a multiple regression approach is typically used: all predictors are entered as columns in a *design-matrix* and the software find the linear combination of these columns that best fits the signal. The weight assigned to each predictor by this linear cobination are estimates of the contribution of this predictor to the response in the voxel. One can plot this effect size maps or, maps showing their statistical significance (how unlikely theye are under the null hypothesis of no effect) + +SHOW a beta map and SPMt map side by side. + +.. figure:: images/example-spmZ_map.png + +In brief, the analysis of fMRI images involves: + +1. describing the paradigm in terms of events of various types occuring at certain times and having a certain duration. +2. from these time diagram, creating predictors for each type of event, typically using a convolution by the impulse HRF + +3. assembling these predictors in a design-matrix, thus providing a *model* +4. estimate the parameters of the model, that is, the weights associated to each predictors, at each voxel, using multiple regression. +5. display the coefficients and/or their statistical significance. + + + + +Tutorials +========= For tutorials, please check out the `Examples `_ gallery From db4a9aaf9a4cb8f410205754a2687f5fae26eab0 Mon Sep 17 00:00:00 2001 From: chrplr Date: Fri, 27 Jul 2018 12:00:34 +0200 Subject: [PATCH 026/210] typo in nipype --- doc/introduction.rst | 56 ++++++++++--------- ...lock_design_single_subject_single_bloc.py} | 30 ++++------ 2 files changed, 41 insertions(+), 45 deletions(-) rename examples/01_tutorials/{plot_spm_auditory.py => block_design_single_subject_single_bloc.py} (90%) diff --git a/doc/introduction.rst b/doc/introduction.rst index 84e183e0..72dec5df 100644 --- a/doc/introduction.rst +++ b/doc/introduction.rst @@ -12,7 +12,13 @@ What is nistats? .. topic:: **What is nistats?** - Nistats is a python module to perform voxel-wise analyses of functional magnetic resonance images (fMRI) using linear models. It provides functions to create design matrices, at the subject and group levels, to estimate them from images series and to compute statistical maps (contrasts). It allows to perform the same statistical analyses than SPM or FSL (but it does not provide tools for preprocessing stages (realignement, spatial normalization, etc.); for this, see nipype???). + Nistats is a python module to perform voxel-wise analyses of functional magnetic resonance images (fMRI) using linear models. It provides functions to create design matrices, at the subject and group levels, to estimate them from images series and to compute statistical maps (contrasts). It allows to perform the same statistical analyses as `a SPM`_ or `a FSL`_ (but it does not provide tools for preprocessing stages (realignement, spatial normalization, etc.); for this, see `a nipype`_. + +.. _a SPM: https://www.fil.ion.ucl.ac.uk/spm/ + +.. _a FSL: https://www.fmrib.ox.ac.uk/fsl + +.. _a nipype: https://nipype.readthedocs.io/en/latest/> @@ -23,58 +29,54 @@ Functional magnetic resonance imaging (fMRI) is based on the fact that when loca Brain activity is measured in sessions that last several minutes, during which the participant performs some kind of cognitive task while the scanner acquires brain images, typically every 2 or 3 seconds (the time between two successive image acquisition is called the Time of Repetition, or TR). -A cerebral MR image provides a 3D image of the brain that can be decomposed in voxels (the equivalent of pixels, but in 3 dimensions). - - INSERT HERE A PIC OF A BRAIN WITH A VOXEL GRID SUPERIMPOSED +A cerebral MR image provides a 3D image of the brain that can be decomposed into `a voxels`_ (the equivalent of pixels, but in 3 dimensions). The series of images acquired during a functional session provides, in each voxel, a time series of positive real number representing the MRI signal, sampled at the TR. -A typical step in the preprocessing of MR images, involves spatially morphing these images onto a standard template (e.g. the MNI152 template from the Montreal Neurological Institute). One this is done, the coordinates of a voxel are in the same space as the template and can be used to estimate its brain location using brain atlases. +.. _a voxels: https://en.wikipedia.org/wiki/Voxel -The series of images acquired during a functional session provide, in each voxel, a time series, sampled at the TR. +TODO: INSERT HERE A SAMPLE OF A TIME SERIES IN A VOXEL (or several voxels) -INSERT HERE A SAMPLE OF A TIME SERIES IN A VOXEL (or several voxels) +.. note:: A typical step in the preprocessing of MR images, involves spatially morphing these images onto a standard template (e.g. the MNI152 template from the Montreal Neurological Institute). One this is done, the coordinates of a voxel are in the same space as the template and can be used to estimate its brain location using brain atlases. -One way to analyze such times series consists in comparing them to a *model* buld from our knowledge of the events that occurred during the functional session. Events can correspond to actions of the participant (e.g. button presses), presentations of sensory stimui (e.g. sound, images), or hypothesized internal processes (e.g. memorization of a stimulus), ... +One way to analyze such times series consists in comparing them to a *model* built from our knowledge of the events that occurred during the functional session. Events can correspond to actions of the participant (e.g. button presses), presentations of sensory stimui (e.g. sound, images), or hypothesized internal processes (e.g. memorization of a stimulus), ... -.. INSERT HERE AN IMAGE OF a TIME DIAGRAM OF EVENTS DURING A RUN .. figure:: images/stimulation-time-diagram.png -One expects that a brain region involved in the processing of a certain type of event (e.g. the auditory cortex for sounds), would show a time course of activation that correlates with the time-diagram of these events. If the fMRI signal directly showed neural activity and did not contain any noise, we could just look at it in various voxel and detect those who conform to the time-diagrams. +One expects that a brain region involved in the processing of a certain type of event (e.g. the auditory cortex for sounds), would show a time course of activation that correlates with the time-diagram of these events. If the fMRI signal directly showed neural activity and did not contain any noise, we could just look at it in various voxel and detect those that conform to the time-diagrams. -Yet, we know, from previous measurements, that the BOLD signal does not follow the exact time course of stimulus processing and the underlying neural activity. The BOLD response, reflecting changes in blood flow and concentrations in oxy-deoxy hemoglobin. In reality, it is sluggish and long-lasting. - -.. INSERT A FIG of the iHRF +Yet, we know, from previous measurements, that the BOLD signal does not follow the exact time course of stimulus processing and the underlying neural activity. The BOLD response reflects changes in blood flow and concentrations in oxy-deoxy hemoglobin, all together forming an `a haemodynamic response`_ which is sluggish and long-lasting, as can be seen on the following figurte showing the response to an impulsive event (for example, an auditory click played to the participants). .. figure:: images/spm_iHRF.png +From the knowledge of the impulse haemodynamic response, we can build a predicted time course from the time-diagram of a type of event (The operation is known a a convolution. Remark: it assumes linearity of the BOLD response, an assumption that may be wrong in some scenarios). It is this predicted time course, also known as a *predictor*, that is compared to the actual fMRI signal. If the correlation between the predictor and the signal is higher than expected by chance, the voxel is said to exhibit a significant response to the event type. -From the knowledge of the impulse haemodynamic response (FIG), we can build a predicted time course from the time-diagram of a type of event (The operation is known a a convolution. Remark: it assumes linearity of the BOLD response, a assumption that may be wrong in some scenarios). It is this predicted time course, also known as a *predictor*, that is compared to the actual fMRI signal. If the correlation between the predictor and the signal is higher than expected by chance, the voxel is said to exhibit a significant response to the event type. - -.. INSERT A FIG SHOWING SIGNAL AND PREDICTOR AND their CORRELATION -.. figure:: images/time-course-and-model-fit-in-a-voxel.png +.. _a haemodynamic response: https://en.wikipedia.org/wiki/Haemodynamic_response -Correlations are computed separately at each voxel and a correlation map can be produced. -INSERT A CORRELATION (BRAIN) MAP HERE. With a certain Threshold. +.. figure:: images/time-course-and-model-fit-in-a-voxel.png -In most fMRI experiments, several predictors described the events. To find the effect specific to each predictor, a multiple regression approach is typically used: all predictors are entered as columns in a *design-matrix* and the software find the linear combination of these columns that best fits the signal. The weight assigned to each predictor by this linear cobination are estimates of the contribution of this predictor to the response in the voxel. One can plot this effect size maps or, maps showing their statistical significance (how unlikely theye are under the null hypothesis of no effect) +Correlations are computed separately at each voxel and a correlation map can be produced displaying the values of correlations (real numbers between -1 and +1) at each voxel. Generally, however, the maps presented in the papers report the significance of the correlations at each voxel, in forms of T, Z or p values for the null hypothesis test of no correlation. For example, the following figure didsplays a Z-map showing voxels responding to auditory events. Large (positive or negative) Z values are unlikely to be due to chance alone. The map is trasholded so that only voxels with a p-value less than 1/1000 are coloured. -SHOW a beta map and SPMt map side by side. +.. note:: + Because, in this approach, hypothesis tests are conducted in parallel at many voxels, the likelyhood of making false alarms is important. This is knon as the probmlem of multiple comparisons. It is beyond the scope of this short notice to eplain ways in which this problem can be approached. Let us just mention that this issue can be addressed in nistats by using randnom permutations tests. .. figure:: images/example-spmZ_map.png -In brief, the analysis of fMRI images involves: -1. describing the paradigm in terms of events of various types occuring at certain times and having a certain duration. -2. from these time diagram, creating predictors for each type of event, typically using a convolution by the impulse HRF +In most fMRI experiments, several predictors are needed to fullly describe the events occuring during the session -- for example, the experimenter may want to distinguish brain activities linked to the perception of auditory stimuli or to button presses. To find the effect specific to each predictor, a multiple `a linear regression`_ approach is typically used: all predictors are entered as columns in a *design-matrix* and the software finds the linear combination of these columns that best fits the signal. The weights assigned to each predictor by this linear cobination are estimates of the contribution of this predictor to the response in the voxel. One can plot this effect size maps or, maps showing their statistical significance (how unlikely theey are under the null hypothesis of no effect). -3. assembling these predictors in a design-matrix, thus providing a *model* -4. estimate the parameters of the model, that is, the weights associated to each predictors, at each voxel, using multiple regression. -5. display the coefficients and/or their statistical significance. +.. _a linear regression: https://en.wikipedia.org/wiki/Linear_regression +In brief, the analysis of fMRI images involves: + +1. describing the paradigm in terms of events of various types occuring at certain times and having a certain duration. +2. from this description, creating predictors for each type of event, typically using a convolution by the impulse haemodynamic response. +3. assembling these predictors in a design-matrix, providing a *linear model* +4. estimate the parameters of the model, that is, the weights associated with each predictors at each voxel, using linear regression. +5. display the coefficients, or linear combination of them, and/or their statistical significance. Tutorials diff --git a/examples/01_tutorials/plot_spm_auditory.py b/examples/01_tutorials/block_design_single_subject_single_bloc.py similarity index 90% rename from examples/01_tutorials/plot_spm_auditory.py rename to examples/01_tutorials/block_design_single_subject_single_bloc.py index a8a1a361..e4038f1d 100644 --- a/examples/01_tutorials/plot_spm_auditory.py +++ b/examples/01_tutorials/block_design_single_subject_single_bloc.py @@ -1,28 +1,22 @@ """ -Univariate analysis of block design, one condition versus rest, single subject -============================================================================== +Analysis of a block design (stimulation vs rest), single session, single subject. +================================================================================= In this tutorial, we compare the fMRI signal during periods of auditory -stimulation versus periods of rest, using a General Linear Model (GLM). We will -use a univariate approach in which independent tests are performed at -each single-voxel. +stimulation versus periods of rest, using a General Linear Model (GLM). -The dataset comes from experiment conducted at the FIL by Geriant Rees +The dataset comes from an experiment conducted at the FIL by Geriant Rees under the direction of Karl Friston. It is provided by FIL methods group which develops the SPM software. -According to SPM documentation, 96 acquisitions were made (RT=7s), in -blocks of 6, giving 16 42s blocks. The condition for successive blocks -alternated between rest and auditory stimulation, starting with rest. -Auditory stimulation was bi-syllabic words presented binaurally at a -rate of 60 per minute. The functional data starts at acquisiton 4, -image fM00223_004. - -The whole brain BOLD/EPI images were acquired on a modified 2T Siemens -MAGNETOM Vision system. Each acquisition consisted of 64 contiguous -slices (64x64x64 3mm x 3mm x 3mm voxels). Acquisition took 6.05s, with -the scan to scan repeat time (RT) set arbitrarily to 7s. +According to SPM documentation, 96 scans were acquired (RT=7s) in one session. THe paradigm consisted of alternating periods of stimulation and rest, lasting 42s each (that is, for 6 scans). The sesssion started with a rest block. +Auditory stimulation consisted of bi-syllabic words presented binaurally at a +rate of 60 per minute. The functional data starts at scan number 4, that is the +image file ``fM00223_004``. +The whole brain BOLD/EPI images were acquired on a 2T Siemens +MAGNETOM Vision system. Each scan consisted of 64 contiguous +slices (64x64x64 3mm x 3mm x 3mm voxels). Acquisition of one scan took 6.05s, with the scan to scan repeat time (RT) set arbitrarily to 7s. This analyse described here is performed in the native space, on the original EPI scans without any spatial or temporal preprocessing. @@ -31,7 +25,7 @@ To run this example, you must launch IPython via ``ipython ---matplotlib`` in a terminal, or use the Jupyter notebook. +--matplotlib`` in a terminal, or use ``jupyter-notebook``. .. contents:: **Contents** :local: From 6b02f11a03d7afca23e9169121b33b7dd6604116 Mon Sep 17 00:00:00 2001 From: chrplr Date: Fri, 27 Jul 2018 13:39:43 +0200 Subject: [PATCH 027/210] intro & first tuto --- doc/images/stimulation-time-diagram.svg | 288 ++++++++++-------- doc/introduction.rst | 12 +- .../01_tutorials/auditory_block_paradigm.csv | 17 ++ ...block_design_single_subject_single_bloc.py | 159 ++++------ .../extracting-signal-from-a-voxel.py | 38 +++ 5 files changed, 294 insertions(+), 220 deletions(-) create mode 100644 examples/01_tutorials/auditory_block_paradigm.csv create mode 100644 examples/01_tutorials/extracting-signal-from-a-voxel.py diff --git a/doc/images/stimulation-time-diagram.svg b/doc/images/stimulation-time-diagram.svg index 01acf8bc..8b6d46c3 100644 --- a/doc/images/stimulation-time-diagram.svg +++ b/doc/images/stimulation-time-diagram.svg @@ -1,17 +1,41 @@ + + + viewBox="0 0 744.09448819 1052.3622047" + id="svg2" + version="1.1" + inkscape:version="0.91 r13725" + sodipodi:docname="stimulation-time-diagram.svg"> + @@ -25,188 +49,212 @@ + id="path3336" + inkscape:connector-curvature="0" /> + id="path3338" + inkscape:connector-curvature="0" /> + id="path3336-3" + inkscape:connector-curvature="0" /> + id="path3338-6" + inkscape:connector-curvature="0" /> + id="path3336-7" + inkscape:connector-curvature="0" /> + id="path3338-5" + inkscape:connector-curvature="0" /> sound Sound + y="221.00667">Sound Buttons duskDisplay Display + y="153.32645">Display + height="6.060915" + x="159.6041" + y="144.23506" /> - + + height="8.0812206" + x="506.08643" + y="141.18431" /> + height="11.111678" + x="200.01021" + y="204.82391" /> + height="8.0812206" + x="564.67529" + y="204.84422" /> - + + height="7.0710678" + x="534.87579" + y="271.00922" /> Beep + y="198.78329">Beep tada + y="200.8036">tada left + y="267.47369">left right + y="266.46353">right picture1 picture2 + y="138.17415">picture2 probe + y="138.17415">probe diff --git a/doc/introduction.rst b/doc/introduction.rst index 72dec5df..0b8cd701 100644 --- a/doc/introduction.rst +++ b/doc/introduction.rst @@ -25,19 +25,19 @@ What is nistats? A primer on BOLD-fMRI data analysis =================================== -Functional magnetic resonance imaging (fMRI) is based on the fact that when local neural activity increases, increases in metabolism and blood flow lead to fluctuations of the relative concentrations of oxyhemoglobine (the red cells in the blood that carry oxygen) and deoxyhemoglobine (the same red cells after they have delivered the oxygen). Because oxy and deoxy hemoglobine have different magnetic properties (one is diamagnetic while the other is paramagnetic), they affect the local magnetic field in different ways. The signal picked up by the MRI scanner is sensitive to these modifications of the local magnetic field. To record cerebral activity, during functional sessions, the scanner is tuned to detect this "Blood Oxygen Level Dependent" (BOLD) contrast. +Functional magnetic resonance imaging (fMRI) is based on the fact that when local neural activity increases, increases in metabolism and blood flow lead to fluctuations of the relative concentrations of oxyhemoglobin (the red cells in the blood that carry oxygen) and deoxyhemoglobin (the same red cells after they have delivered the oxygen). Because oxy- and deoxy-hemoglobin have different magnetic properties (one is diamagnetic while the other is paramagnetic), they affect the local magnetic field in different ways. The signal picked up by the MRI scanner is sensitive to these modifications of the local magnetic field. To record cerebral activity, during functional sessions, the scanner is tuned to detect this "Blood Oxygen Level Dependent" (BOLD) signal. -Brain activity is measured in sessions that last several minutes, during which the participant performs some kind of cognitive task while the scanner acquires brain images, typically every 2 or 3 seconds (the time between two successive image acquisition is called the Time of Repetition, or TR). +Brain activity is measured in sessions that span several minutes, while the participant performs some a cognitive task and the scanner acquires brain images, typically every 2 or 3 seconds (the time between two successive image acquisition is called the Repetition time, or RT). -A cerebral MR image provides a 3D image of the brain that can be decomposed into `a voxels`_ (the equivalent of pixels, but in 3 dimensions). The series of images acquired during a functional session provides, in each voxel, a time series of positive real number representing the MRI signal, sampled at the TR. +A cerebral MR image provides a 3D image of the brain that can be decomposed into `a voxels`_ (the equivalent of pixels, but in 3 dimensions). The series of images acquired during a functional session provides, in each voxel, a time series of positive real number representing the MRI signal, sampled at the RT. .. _a voxels: https://en.wikipedia.org/wiki/Voxel TODO: INSERT HERE A SAMPLE OF A TIME SERIES IN A VOXEL (or several voxels) -.. note:: A typical step in the preprocessing of MR images, involves spatially morphing these images onto a standard template (e.g. the MNI152 template from the Montreal Neurological Institute). One this is done, the coordinates of a voxel are in the same space as the template and can be used to estimate its brain location using brain atlases. +.. note:: A typical step in the preprocessing of MR images, involves spatially morphing these images onto a standard template (e.g. the MNI152 template from the Montreal Neurological Institute). One this is done, the coordinates of a voxel are in the same space as the template and can be used to estimate its brain location using brain atlases. As already mentioned, the nistats package is not meant to perform spiatal preprocessing, but only statistical analyses on the voxel times series, regardless of the coordinate system. -One way to analyze such times series consists in comparing them to a *model* built from our knowledge of the events that occurred during the functional session. Events can correspond to actions of the participant (e.g. button presses), presentations of sensory stimui (e.g. sound, images), or hypothesized internal processes (e.g. memorization of a stimulus), ... +One way to analyze times series consists in comparing them to a *model* built from our knowledge of the events that occurred during the functional session. Events can correspond to actions of the participant (e.g. button presses), presentations of sensory stimui (e.g. sound, images), or hypothesized internal processes (e.g. memorization of a stimulus), ... .. figure:: images/stimulation-time-diagram.png @@ -72,7 +72,7 @@ In most fMRI experiments, several predictors are needed to fullly describe the In brief, the analysis of fMRI images involves: -1. describing the paradigm in terms of events of various types occuring at certain times and having a certain duration. +1. describing the paradigm in terms of events of various types occuring at certain times and having some durations. 2. from this description, creating predictors for each type of event, typically using a convolution by the impulse haemodynamic response. 3. assembling these predictors in a design-matrix, providing a *linear model* 4. estimate the parameters of the model, that is, the weights associated with each predictors at each voxel, using linear regression. diff --git a/examples/01_tutorials/auditory_block_paradigm.csv b/examples/01_tutorials/auditory_block_paradigm.csv new file mode 100644 index 00000000..9511381d --- /dev/null +++ b/examples/01_tutorials/auditory_block_paradigm.csv @@ -0,0 +1,17 @@ +duration,onset,trial_type +42.0,0.0,rest +42.0,42.0,active +42.0,84.0,rest +42.0,126.0,active +42.0,168.0,rest +42.0,210.0,active +42.0,252.0,rest +42.0,294.0,active +42.0,336.0,rest +42.0,378.0,active +42.0,420.0,rest +42.0,462.0,active +42.0,504.0,rest +42.0,546.0,active +42.0,588.0,rest +42.0,630.0,active diff --git a/examples/01_tutorials/block_design_single_subject_single_bloc.py b/examples/01_tutorials/block_design_single_subject_single_bloc.py index e4038f1d..678534cb 100644 --- a/examples/01_tutorials/block_design_single_subject_single_bloc.py +++ b/examples/01_tutorials/block_design_single_subject_single_bloc.py @@ -18,7 +18,7 @@ MAGNETOM Vision system. Each scan consisted of 64 contiguous slices (64x64x64 3mm x 3mm x 3mm voxels). Acquisition of one scan took 6.05s, with the scan to scan repeat time (RT) set arbitrarily to 7s. -This analyse described here is performed in the native space, on the +The analyse described here is performed in the native space, directly on the original EPI scans without any spatial or temporal preprocessing. (More sensitive results would likely be obtained on the corrected, spatially normalized and smoothed images). @@ -40,95 +40,99 @@ # ------------------- # # .. note:: In this tutorial, we load the data using a data downloading -# function.To input your own data, you will need to pass -# a list of paths to your own files. +# function. To input your own data, you will need to provide +# a list of paths to your own files in the ``subject_data`` variable. from nistats.datasets import fetch_spm_auditory subject_data = fetch_spm_auditory() - - +print(subject_data.func) # print the list of names of functional images + ############################################################################### -# We can list the filenames of the functional images -print(subject_data.func) - -############################################################################### -# Display the first functional image: +# We can display the first functional image and the subject's anatomy: from nilearn.plotting import plot_stat_map, plot_anat, plot_img plot_img(subject_data.func[0]) - -############################################################################### -# Display the subject's anatomical image: plot_anat(subject_data.anat) - -############################################################################### -# Next, we concatenate all the 3D EPI image into a single 4D image: - -from nilearn.image import concat_imgs -fmri_img = concat_imgs(subject_data.func) - ############################################################################### -# And we average all the EPI images in order to create a background +# Next, we concatenate all the 3D EPI image into a single 4D image, +# the we average them in order to create a background # image that will be used to display the activations: -from nilearn import image -mean_img = image.mean_img(fmri_img) +from nilearn.image import concat_imgs, mean_img +fmri_img = concat_imgs(subject_data.func) +mean_img = mean_img(fmri_img) ############################################################################### # Specifying the experimental paradigm # ------------------------------------ # -# We must provide a description of the experiment, that is, define the +# We must provide now a description of the experiment, that is, define the # timing of the auditory stimulation and rest periods. According to -# the documentation of the dataset, there were 16 42s blocks --- in +# the documentation of the dataset, there were sixteen 42s-long blocks --- in # which 6 scans were acquired --- alternating between rest and -# auditory stimulation, starting with rest. We use standard python -# functions to create a pandas.DataFrame object that specifies the -# timings: +# auditory stimulation, starting with rest. +# +# The following table provide all the relevant informations: +# -import numpy as np -tr = 7. -slice_time_ref = 0. -n_scans = 96 -epoch_duration = 6 * tr # duration in seconds -conditions = ['rest', 'active'] * 8 -n_blocks = len(conditions) -duration = epoch_duration * np.ones(n_blocks) -onset = np.linspace(0, (n_blocks - 1) * epoch_duration, n_blocks) +""" +duration, onset, trial_type + 42 , 0 , rest + 42 , 42 , active + 42 , 84 , rest + 42 , 126 , active + 42 , 168 , rest + 42 , 210 , active + 42 , 252 , rest + 42 , 294 , active + 42 , 336 , rest + 42 , 378 , active + 42 , 420 , rest + 42 , 462 , active + 42 , 504 , rest + 42 , 546 , active + 42 , 588 , rest + 42 , 630 , active +""" -import pandas as pd -events = pd.DataFrame( - {'onset': onset, 'duration': duration, 'trial_type': conditions}) +# We can read such a table from a spreadsheet file created with OpenOffice Calcor Office Excel, and saved under the *comma separated values* format (``.csv``). -############################################################################### -# The ``events`` object contains the information for the design: +import pandas as pd +events = pd.read_csv('auditory_block_paradigm.csv') print(events) +# ## ################################################################### +# # Alternatively, we could have used standard python +# # functions to create a pandas.DataFrame object that specifies the +# # timings: + +# import numpy as np +# tr = 7. +# slice_time_ref = 0. +# n_scans = 96 +# epoch_duration = 6 * tr # duration in seconds +# conditions = ['rest', 'active'] * 8 +# n_blocks = len(conditions) +# duration = epoch_duration * np.ones(n_blocks) +# onset = np.linspace(0, (n_blocks - 1) * epoch_duration, n_blocks) +# events = pd.DataFrame( +# {'onset': onset, 'duration': duration, 'trial_type': conditions}) + ############################################################################### # Performing the GLM analysis # --------------------------- # -# We need to construct a *design matrix* using the timing information -# provided by the ``events`` object. The design matrix contains -# regressors of interest as well as regressors of non-interest -# modeling temporal drifts: - -frame_times = np.linspace(0, (n_scans - 1) * tr, n_scans) -drift_model = 'Cosine' -period_cut = 4. * epoch_duration -hrf_model = 'glover + derivative' - -############################################################################### -# It is now time to create a ``FirstLevelModel`` object -# and fit it to the 4D dataset (Fitting means that the coefficients of the -# model are estimated to best approximate data) +# It is now time to create and estimate a ``FirstLevelModel`` object, which will# generate the *design matrix* using the information provided by the ``events` object. from nistats.first_level_model import FirstLevelModel -fmri_glm = FirstLevelModel(tr, slice_time_ref, noise_model='ar1', - standardize=False, hrf_model=hrf_model, - drift_model=drift_model, period_cut=period_cut) +fmri_glm = FirstLevelModel(t_r=7, + noise_model='ar1', + standardize=False, + hrf_model='spm', + drift_model='cosine', + period_cut=160) fmri_glm = fmri_glm.fit(fmri_img, events) ############################################################################### @@ -144,7 +148,6 @@ # The first column contains the expected reponse profile of regions which are # sensitive to the auditory stimulation. - plt.plot(design_matrix['active']) plt.xlabel('scan') plt.title('Expected Auditory Response') @@ -157,7 +160,9 @@ # # To access the estimated coefficients (Betas of the GLM model), we # created constrasts with a single '1' in each of the columns: +# TODO: simplify!!! +import numpy as np contrast_matrix = np.eye(design_matrix.shape[1]) contrasts = dict([(column, contrast_matrix[i]) for i, column in enumerate(design_matrix.columns)]) @@ -213,37 +218,3 @@ nibabel.save(z_map, join('results', 'active_vs_rest_z_map.nii')) nibabel.save(eff_map, join('results', 'active_vs_rest_eff_map.nii')) -############################################################################### -# Extract the signal from a voxel -# ------------------------------- -# -# We search for the voxel with the larger z-score and plot the signal -# (warning: this is "double dipping") - - -# Find the coordinates of the peak - -from nibabel.affines import apply_affine -values = z_map.get_data() -coord_peaks = np.dstack(np.unravel_index(np.argsort(-values.ravel()), - values.shape))[0, 0, :] -coord_mm = apply_affine(z_map.affine, coord_peaks) - -############################################################################### -# We create a masker for the voxel (allowing us to detrend the signal) -# and extract the time course - -from nilearn.input_data import NiftiSpheresMasker -mask = NiftiSpheresMasker([coord_mm], radius=3, - detrend=True, standardize=True, - high_pass=None, low_pass=None, t_r=7.) -sig = mask.fit_transform(fmri_img) - -########################################################## -# Let's plot the signal and the theoretical response - -plt.plot(frame_times, sig, label='voxel %d %d %d' % tuple(coord_mm)) -plt.plot(design_matrix['active'], color='red', label='model') -plt.xlabel('scan') -plt.legend() -plt.show() diff --git a/examples/01_tutorials/extracting-signal-from-a-voxel.py b/examples/01_tutorials/extracting-signal-from-a-voxel.py new file mode 100644 index 00000000..b841c4c5 --- /dev/null +++ b/examples/01_tutorials/extracting-signal-from-a-voxel.py @@ -0,0 +1,38 @@ +#! /usr/bin/env python +# TODO +# This follows the singlesubject/single (spm_auditory) session example + +############################################################################### +# Extract the signal from a voxel +# ------------------------------- +# +# We search for the voxel with the larger z-score and plot the signal +# (warning: this is "double dipping") + + +# Find the coordinates of the peak + +from nibabel.affines import apply_affine +values = z_map.get_data() +coord_peaks = np.dstack(np.unravel_index(np.argsort(-values.ravel()), + values.shape))[0, 0, :] +coord_mm = apply_affine(z_map.affine, coord_peaks) + +############################################################################### +# We create a masker for the voxel (allowing us to detrend the signal) +# and extract the time course + +from nilearn.input_data import NiftiSpheresMasker +mask = NiftiSpheresMasker([coord_mm], radius=3, + detrend=True, standardize=True, + high_pass=None, low_pass=None, t_r=7.) +sig = mask.fit_transform(fmri_img) + +########################################################## +# Let's plot the signal and the theoretical response + +plt.plot(frame_times, sig, label='voxel %d %d %d' % tuple(coord_mm)) +plt.plot(design_matrix['active'], color='red', label='model') +plt.xlabel('scan') +plt.legend() +plt.show() From a0b716b5c89d88be1828b5795b4571e7898605c5 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Fri, 3 Aug 2018 11:16:01 -0400 Subject: [PATCH 028/210] ENH: Replace DataFrame.as_matrix() with .values --- nistats/design_matrix.py | 2 +- nistats/first_level_model.py | 2 +- nistats/second_level_model.py | 4 ++-- nistats/tests/test_first_level_model.py | 2 +- nistats/tests/test_second_level_model.py | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/nistats/design_matrix.py b/nistats/design_matrix.py index 696acdbf..f5fdba73 100644 --- a/nistats/design_matrix.py +++ b/nistats/design_matrix.py @@ -478,7 +478,7 @@ def create_second_level_design(subjects_label, confounds=None): # check design matrix is not singular epsilon = sys.float_info.epsilon - if np.linalg.cond(design_matrix.as_matrix()) > design_matrix.size: + if np.linalg.cond(design_matrix.values) > design_matrix.size: warn('Attention: Design matrix is singular. Aberrant estimates ' 'are expected.') return design_matrix diff --git a/nistats/first_level_model.py b/nistats/first_level_model.py index d4e9902d..5f7a715d 100644 --- a/nistats/first_level_model.py +++ b/nistats/first_level_model.py @@ -462,7 +462,7 @@ def fit(self, run_imgs, events=None, confounds=None, if self.verbose > 1: t_glm = time.time() sys.stderr.write('Performing GLM computation\r') - labels, results = mem_glm(Y, design.as_matrix(), + labels, results = mem_glm(Y, design.values, noise_model=self.noise_model, bins=100, n_jobs=self.n_jobs) if self.verbose > 1: diff --git a/nistats/second_level_model.py b/nistats/second_level_model.py index e2bda531..58b71696 100644 --- a/nistats/second_level_model.py +++ b/nistats/second_level_model.py @@ -249,7 +249,7 @@ def fit(self, second_level_input, confounds=None, design_matrix=None): columns = second_level_input.columns.tolist() column_index = columns.index('subject_label') sorted_matrix = sorted( - second_level_input.as_matrix(), key=lambda x: x[column_index]) + second_level_input.values, key=lambda x: x[column_index]) sorted_input = pd.DataFrame(sorted_matrix, columns=columns) second_level_input = sorted_input @@ -402,7 +402,7 @@ def compute_contrast( mem_glm = self.memory.cache(run_glm, ignore=arg_ignore) else: mem_glm = run_glm - labels, results = mem_glm(Y, self.design_matrix_.as_matrix(), + labels, results = mem_glm(Y, self.design_matrix_.values, n_jobs=self.n_jobs, noise_model='ols') # We save memory if inspecting model details is not necessary if self.minimize_memory: diff --git a/nistats/tests/test_first_level_model.py b/nistats/tests/test_first_level_model.py index 6a4b0827..6e3b36ef 100644 --- a/nistats/tests/test_first_level_model.py +++ b/nistats/tests/test_first_level_model.py @@ -269,7 +269,7 @@ def test_first_level_model_glm_computation(): results1 = model.results_[0] labels2, results2 = run_glm( model.masker_.transform(func_img), - model.design_matrices_[0].as_matrix(), 'ar1') + model.design_matrices_[0].values, 'ar1') # ar not giving consistent results in python 3.4 # assert_almost_equal(labels1, labels2, decimal=2) ####FIX # assert_equal(len(results1), len(results2)) ####FIX diff --git a/nistats/tests/test_second_level_model.py b/nistats/tests/test_second_level_model.py index 4faf5a1f..3abcaf95 100644 --- a/nistats/tests/test_second_level_model.py +++ b/nistats/tests/test_second_level_model.py @@ -157,7 +157,7 @@ def test_second_level_model_glm_computation(): results1 = model.results_ labels2, results2 = run_glm( - model.masker_.transform(Y), X.as_matrix(), 'ols') + model.masker_.transform(Y), X.values, 'ols') assert_almost_equal(labels1, labels2, decimal=1) assert_equal(len(results1), len(results2)) From 9dc2a15a3faf0da4c8bb6fefc55f46360e0a9aac Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Tue, 28 Aug 2018 15:37:42 +0200 Subject: [PATCH 029/210] Created CircleCI 2.0 config.yml, decactivated v1.0 circle.yml --- .circleci/config.yml | 59 ++++++++++++++++++++++++++++++++++++ circle.yml => circle_old.yml | 0 2 files changed, 59 insertions(+) create mode 100644 .circleci/config.yml rename circle.yml => circle_old.yml (100%) diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 00000000..5d927e03 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,59 @@ +version: 2 + +jobs: + build: + docker: + - image: circleci/python:3.6 + environment: + DISTRIB: "conda" + PYTHON_VERSION: "3.6" + NUMPY_VERSION: "*" + SCIPY_VERSION: "*" + SCIKIT_LEARN_VERSION: "*" + MATPLOTLIB_VERSION: "*" + + steps: + - checkout + # Get rid of existing virtualenvs on circle ci as they conflict with conda. + # Trick found here: + # https://discuss.circleci.com/t/disable-autodetection-of-project-or-application-of-python-venv/235/10 + - run: cd && rm -rf ~/.pyenv && rm -rf ~/virtualenvs + # We need to remove conflicting texlive packages. + - run: sudo -E apt-get -yq remove texlive-binaries --purge + # Installing required packages for `make -C doc check command` to work. + - run: sudo -E apt-get -yq update + - run: sudo -E apt-get -yq --no-install-suggests --no-install-recommends --force-yes install dvipng texlive-latex-base texlive-latex-extra + - restore_cache: + key: v1-packages+datasets-{{ .Branch }} + - run: wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O ~/miniconda.sh + - run: chmod +x ~/miniconda.sh && ~/miniconda.sh -b + - run: + name: Setup conda path in env variables + command: | + echo 'export PATH="$HOME/miniconda3/bin:$PATH"' >> $BASH_ENV + - run: + name: Create conda env + command: | + conda create -n testenv python=3.6 numpy scipy scikit-learn matplotlib pandas \ + flake8 lxml nose cython mkl sphinx coverage pillow pandas -yq + conda install -n testenv nibabel nilearn nose-timer -c conda-forge -yq + - run: + name: Running CircleCI test (make html) + command: | + source activate testenv + pip install -e . + set -o pipefail && cd doc && make html-strict 2>&1 | tee ~/log.txt + no_output_timeout: 5h + - save_cache: + key: v1-packages+datasets-{{ .Branch }} + paths: + - $HOME/nilearn_data + - $HOME/miniconda3 + + - store_artifacts: + path: doc/_build/html + - store_artifacts: + path: coverage + - store_artifacts: + path: $HOME/log.txt + destination: log.txt diff --git a/circle.yml b/circle_old.yml similarity index 100% rename from circle.yml rename to circle_old.yml From f0b1d77e719bb7395c571c962d85a8c1cce61d07 Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Tue, 28 Aug 2018 16:37:46 +0200 Subject: [PATCH 030/210] Added patsy & boto3 to the installed dependencies --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 5d927e03..ddbef3d5 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -35,7 +35,7 @@ jobs: name: Create conda env command: | conda create -n testenv python=3.6 numpy scipy scikit-learn matplotlib pandas \ - flake8 lxml nose cython mkl sphinx coverage pillow pandas -yq + flake8 lxml nose cython mkl sphinx coverage patsy boto3 pillow pandas -yq conda install -n testenv nibabel nilearn nose-timer -c conda-forge -yq - run: name: Running CircleCI test (make html) From 8f44bd41a71df9a8568757cd2515eda129d3ecd9 Mon Sep 17 00:00:00 2001 From: bthirion Date: Tue, 28 Aug 2018 21:54:34 +0200 Subject: [PATCH 031/210] Changed second level model not to impose an intercept in thed esign matrix --- nistats/second_level_model.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/nistats/second_level_model.py b/nistats/second_level_model.py index 58b71696..17990cf0 100644 --- a/nistats/second_level_model.py +++ b/nistats/second_level_model.py @@ -155,7 +155,6 @@ def fit(self, second_level_input, confounds=None, design_matrix=None): from second_level_input. Ensure that the order of maps given by a second_level_input list of Niimgs matches the order of the rows in the design matrix. - Must contain a column of 1s with column name 'intercept'. """ # Check parameters # check first level input @@ -238,8 +237,6 @@ def fit(self, second_level_input, confounds=None, design_matrix=None): if design_matrix is not None: if not isinstance(design_matrix, pd.DataFrame): raise ValueError('design matrix must be a pandas DataFrame') - if 'intercept' not in design_matrix.columns: - raise ValueError('design matrix must contain "intercept"') # sort a pandas dataframe by subject_label to avoid inconsistencies # with the design matrix row order when automatically extracting maps @@ -309,7 +306,7 @@ def fit(self, second_level_input, confounds=None, design_matrix=None): return self def compute_contrast( - self, second_level_contrast='intercept', first_level_contrast=None, + self, second_level_contrast=None, first_level_contrast=None, second_level_stat_type=None, output_type='z_score'): """Generate different outputs corresponding to the contrasts provided e.g. z_map, t_map, effects and variance. @@ -324,11 +321,10 @@ def compute_contrast( the fitted model combined with operators /*+- and numbers. Please check the patsy documentation for formula examples: http://patsy.readthedocs.io/en/latest/API-reference.html#patsy.DesignInfo.linear_constraint - - VERY IMPORTANT: The 'intercept' corresponds to the second level - effect after taking confounders in consideration. If there are - no confounders then this will be equivalent to a simple t test. - By default we compute the 'intercept' second level contrast. + The default (None) is accepted if the design matrix has a single + column, in which case the only possible contrast array([1]) is + applied; when the design matrix has multiple columns, an error is + raised. first_level_contrast: str or array of shape (n_col) with respect to FirstLevelModel, optional @@ -366,6 +362,11 @@ def compute_contrast( 'compute_contrast method of FirstLevelModel') # check contrast definition + if second_level_contrast is None: + if self.design_matrix_.shape[1] == 1: + second_level_contrast = np.ones([1]) + else: + raise ValueError('No second-level contrast is specified.') if isinstance(second_level_contrast, np.ndarray): con_val = second_level_contrast if np.all(con_val == 0): From e68ca748b7ce601c60a239fd74a14279a5fea482 Mon Sep 17 00:00:00 2001 From: bthirion Date: Tue, 28 Aug 2018 23:11:35 +0200 Subject: [PATCH 032/210] Added corresponding test --- nistats/tests/test_second_level_model.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/nistats/tests/test_second_level_model.py b/nistats/tests/test_second_level_model.py index 3abcaf95..e3b9151f 100644 --- a/nistats/tests/test_second_level_model.py +++ b/nistats/tests/test_second_level_model.py @@ -196,3 +196,8 @@ def test_second_level_model_contrast_computation(): assert_raises(ValueError, model.compute_contrast, c1, None, '') assert_raises(ValueError, model.compute_contrast, c1, None, []) assert_raises(ValueError, model.compute_contrast, c1, None, None, '') + # check that passing no explicit contrast when the dsign + # matrix has morr than one columns raises an error + X = pd.DataFrame(np.random.rand(4, 2), columns=['r1', 'r2']) + model = model.fit(Y, design_matrix=X) + assert_raises(ValueError, model.compute_contrast, None) From dc39f313a87ce1a14d9a0597ad2bc10943856873 Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Wed, 29 Aug 2018 16:47:17 +0200 Subject: [PATCH 033/210] Added path to backreferences_dir --- doc/conf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/conf.py b/doc/conf.py index e4b1a478..fdcdf050 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -281,6 +281,7 @@ sphinx_gallery_conf = { 'doc_module' : 'nistats', + 'backreferences_dir': os.path.join('modules', 'generated'), 'reference_url' : { 'nilearn': 'http://nilearn.github.io', 'matplotlib': 'http://matplotlib.org', From 8dd85481506869fafc1f64a751045a65b7aa97d8 Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Wed, 29 Aug 2018 18:12:29 +0200 Subject: [PATCH 034/210] Updated miniconda installer to miniconda3 to fix Pandas incorrect version problem - Same error as https://github.com/nilearn/nilearn/issues/1672 --- continuous_integration/install.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/continuous_integration/install.sh b/continuous_integration/install.sh index 3ce0906d..45aaff11 100755 --- a/continuous_integration/install.sh +++ b/continuous_integration/install.sh @@ -61,10 +61,10 @@ create_new_conda_env() { # Use the miniconda installer for faster download / install of conda # itself - wget http://repo.continuum.io/miniconda/Miniconda-latest-Linux-x86_64.sh \ + wget http://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh \ -O miniconda.sh chmod +x miniconda.sh && ./miniconda.sh -b - export PATH=/home/travis/miniconda2/bin:$PATH + export PATH=/home/travis/miniconda3/bin:$PATH echo $PATH conda update --quiet --yes conda From 1f299f0f27514fcd0f38faf083084eba9258a1ca Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Thu, 30 Aug 2018 11:31:17 +0200 Subject: [PATCH 035/210] Updated dependencies+miniconda- align with NiLearn, fix incorrect Pandas version - Updated miniconda installer URL to miniconda3 to fix Pandas incorrect version problem (Same error as https://github.com/nilearn/nilearn/issues/1672). - Updated minimum versions of dependencies in nistats/version.py . - Updated versions of dependencies in for TravisCI builds. --- .travis.yml | 26 +++++++++++++++----------- continuous_integration/install.sh | 4 ++-- nistats/version.py | 16 ++++++++-------- 3 files changed, 25 insertions(+), 21 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5932c806..df81a511 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,26 +20,30 @@ matrix: include: # Ubuntu 14.04 version without matplotlib - env: DISTRIB="conda" PYTHON_VERSION="2.7" - NUMPY_VERSION="1.8.2" SCIPY_VERSION="0.14" - SCIKIT_LEARN_VERSION="0.15.0" - PANDAS_VERSION="0.13.0" + NUMPY_VERSION="1.11.2" SCIPY_VERSION="0.17" + SCIKIT_LEARN_VERSION="0.18.0" + PANDAS_VERSION="0.18.0" # Trying to get as close to the minimum required versions while # still having the package version available through conda - env: DISTRIB="conda" PYTHON_VERSION="2.7" - NUMPY_VERSION="1.8.2" SCIPY_VERSION="0.14" - SCIKIT_LEARN_VERSION="0.15.0" MATPLOTLIB_VERSION="1.3.1" - NIBABEL_VERSION="2.0.2" PANDAS_VERSION="0.13.0" COVERAGE="true" + NUMPY_VERSION="1.11.2" SCIPY_VERSION="0.19" + SCIKIT_LEARN_VERSION="0.18" MATPLOTLIB_VERSION="1.5.1" + NIBABEL_VERSION="2.0.2" PANDAS_VERSION="*" COVERAGE="true" # Python 3.4 with intermediary versions - env: DISTRIB="conda" PYTHON_VERSION="3.4" - NUMPY_VERSION="1.8.2" SCIPY_VERSION="0.14.0" - SCIKIT_LEARN_VERSION="0.15.2" MATPLOTLIB_VERSION="1.4.0" - PANDAS_VERSION="*" PATSY_VERSION="*" - # Most recent versions (Python 3) + NUMPY_VERSION="1.11.2" SCIPY_VERSION="0.17" + SCIKIT_LEARN_VERSION="0.18" MATPLOTLIB_VERSION="1.5.1" + PANDAS_VERSION="0.18.0" PATSY_VERSION="*" + # Python 3.5 with latest versions. - env: DISTRIB="conda" PYTHON_VERSION="3.5" NUMPY_VERSION="*" SCIPY_VERSION="*" SCIKIT_LEARN_VERSION="*" MATPLOTLIB_VERSION="*" COVERAGE="true" PANDAS_VERSION="*" - + # Most recent versions (Python 3) + - env: DISTRIB="conda" PYTHON_VERSION="*" + NUMPY_VERSION="*" SCIPY_VERSION="*" + SCIKIT_LEARN_VERSION="*" MATPLOTLIB_VERSION="*" COVERAGE="true" + PANDAS_VERSION="*" install: - source continuous_integration/install.sh diff --git a/continuous_integration/install.sh b/continuous_integration/install.sh index 3ce0906d..45aaff11 100755 --- a/continuous_integration/install.sh +++ b/continuous_integration/install.sh @@ -61,10 +61,10 @@ create_new_conda_env() { # Use the miniconda installer for faster download / install of conda # itself - wget http://repo.continuum.io/miniconda/Miniconda-latest-Linux-x86_64.sh \ + wget http://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh \ -O miniconda.sh chmod +x miniconda.sh && ./miniconda.sh -b - export PATH=/home/travis/miniconda2/bin:$PATH + export PATH=/home/travis/miniconda3/bin:$PATH echo $PATH conda update --quiet --yes conda diff --git a/nistats/version.py b/nistats/version.py index d15f977b..22cd58fc 100644 --- a/nistats/version.py +++ b/nistats/version.py @@ -30,32 +30,32 @@ # in some meaningful order (more => less 'core'). REQUIRED_MODULE_METADATA = ( ('numpy', { - 'min_version': '1.8.2', + 'min_version': '1.11.2', 'install_info': _NISTATS_INSTALL_MSG}), ('scipy', { - 'min_version': '0.14', + 'min_version': '0.17', 'install_info': _NISTATS_INSTALL_MSG}), ('sklearn', { 'pypi_name': 'scikit-learn', - 'min_version': '0.15.0', + 'min_version': '0.18', 'install_info': _NISTATS_INSTALL_MSG}), ('nibabel', { 'min_version': '2.0.2', 'required_at_installation': True, 'install_info': _NISTATS_INSTALL_MSG}), ('nilearn', { - 'min_version': '0.2.0', + 'min_version': '0.4', 'install_info': _NISTATS_INSTALL_MSG}), ('pandas', { - 'min_version': '0.13.0', + 'min_version': '0.18.0', 'install_info': _NISTATS_INSTALL_MSG}), ('patsy', { - 'min_version': '0.2.0', + 'min_version': '0.4.1', 'install_info': _NISTATS_INSTALL_MSG}), ) -OPTIONAL_MATPLOTLIB_MIN_VERSION = '1.3.1' -OPTIONAL_BOTO3_MIN_VERSION = '1.0.0' +OPTIONAL_MATPLOTLIB_MIN_VERSION = '1.5.1' +OPTIONAL_BOTO3_MIN_VERSION = '1.4.0' def _import_module_with_version_check( From 47711fd3bfa16c30b9068b1689672e9c736b7a21 Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Thu, 30 Aug 2018 11:48:38 +0200 Subject: [PATCH 036/210] Bumped libgfortran to 3.0 for compatiblity with other dependency's version bumps --- continuous_integration/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/continuous_integration/install.sh b/continuous_integration/install.sh index 45aaff11..290d60f1 100755 --- a/continuous_integration/install.sh +++ b/continuous_integration/install.sh @@ -33,7 +33,7 @@ print_conda_requirements() { # if yes which version to install. For example: # - for numpy, NUMPY_VERSION is used # - for scikit-learn, SCIKIT_LEARN_VERSION is used - TO_INSTALL_ALWAYS="pip nose libgfortran=1.0=0 nomkl" + TO_INSTALL_ALWAYS="pip nose libgfortran=3.0=0 nomkl" REQUIREMENTS="$TO_INSTALL_ALWAYS" TO_INSTALL_MAYBE="python numpy scipy matplotlib scikit-learn pandas" for PACKAGE in $TO_INSTALL_MAYBE; do From e03e3b19cb3ef7bf23da0e7d139c6c5317b535cf Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Thu, 30 Aug 2018 12:01:38 +0200 Subject: [PATCH 037/210] Fixed miniconda path for circleci builds --- circle.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/circle.yml b/circle.yml index 495ce713..a71789f7 100644 --- a/circle.yml +++ b/circle.yml @@ -1,6 +1,6 @@ machine: environment: - PATH: /home/ubuntu/miniconda2/bin:$PATH + PATH: /home/ubuntu/miniconda3/bin:$PATH dependencies: cache_directories: From 1bcaffed216b051ac38b61d833a08e1bb51af92f Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Thu, 30 Aug 2018 15:52:49 +0200 Subject: [PATCH 038/210] Updated most of the dependency's versions in the README --- README.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.rst b/README.rst index 21100ee6..cfed4574 100644 --- a/README.rst +++ b/README.rst @@ -30,16 +30,16 @@ The required dependencies to use the software are: * Python >= 2.7 * setuptools -* Numpy >= 1.8.2 -* SciPy >= 0.14 +* Numpy >= 1.11.2 +* SciPy >= 0.17 * Nibabel >= 2.0.2 -* Nilearn >= 0.2.0 -* Pandas >= 0.13.0 -* Sklearn >= 0.15.0 -* Patsy >= 0.2.0 +* Nilearn >= 0.4.0 +* Pandas >= 0.15.0 +* Sklearn >= 0.18.0 +* Patsy >= 0.4.1 If you are using nilearn plotting functionalities or running the -examples, matplotlib >= 1.3.1 is required. +examples, matplotlib >= 1.5.1 is required. If you want to run the tests, you need nose >= 1.2.1 and coverage >= 3.6. From 77d43b0e55774500d0e9d601f3ca69a3ff1cd04b Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Fri, 31 Aug 2018 09:24:30 +0200 Subject: [PATCH 039/210] Removed `strict` option from CircleCI make html to complete CCi v2 transition --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index ddbef3d5..253b648a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -42,7 +42,7 @@ jobs: command: | source activate testenv pip install -e . - set -o pipefail && cd doc && make html-strict 2>&1 | tee ~/log.txt + set -o pipefail && cd doc && make html 2>&1 | tee ~/log.txt no_output_timeout: 5h - save_cache: key: v1-packages+datasets-{{ .Branch }} From 8d5d6d45a5b1406e8b74a8a37fdf085fdf11a48e Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Fri, 31 Aug 2018 09:46:05 +0200 Subject: [PATCH 040/210] Updated the dependency versions referred to in code & docs --- README.rst | 8 ++++---- doc/conf.py | 4 ++-- nistats/second_level_model.py | 2 -- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/README.rst b/README.rst index cfed4574..4744e6b0 100644 --- a/README.rst +++ b/README.rst @@ -30,20 +30,20 @@ The required dependencies to use the software are: * Python >= 2.7 * setuptools -* Numpy >= 1.11.2 +* Numpy >= 1.11 * SciPy >= 0.17 * Nibabel >= 2.0.2 * Nilearn >= 0.4.0 -* Pandas >= 0.15.0 +* Pandas >= 0.18.0 * Sklearn >= 0.18.0 * Patsy >= 0.4.1 If you are using nilearn plotting functionalities or running the examples, matplotlib >= 1.5.1 is required. -If you want to run the tests, you need nose >= 1.2.1 and coverage >= 3.6. +If you want to run the tests, you need nose >= 1.2.1 and coverage >= 3.7. -If you want to download openneuro datasets Boto3 >= 1.0.0 is required +If you want to download openneuro datasets Boto3 >= 1.2 is required Install diff --git a/doc/conf.py b/doc/conf.py index e4b1a478..f65d5881 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -284,8 +284,8 @@ 'reference_url' : { 'nilearn': 'http://nilearn.github.io', 'matplotlib': 'http://matplotlib.org', - 'numpy': 'http://docs.scipy.org/doc/numpy-1.6.0', - 'scipy': 'http://docs.scipy.org/doc/scipy-0.11.0/reference', + 'numpy': 'http://docs.scipy.org/doc/numpy-1.11.0', + 'scipy': 'http://docs.scipy.org/doc/scipy-0.17.0/reference', 'nibabel': 'http://nipy.org/nibabel', 'sklearn': 'http://scikit-learn.org/stable', 'patsy': 'http://patsy.readthedocs.io/en/latest/', diff --git a/nistats/second_level_model.py b/nistats/second_level_model.py index 58b71696..7e7c6a40 100644 --- a/nistats/second_level_model.py +++ b/nistats/second_level_model.py @@ -244,8 +244,6 @@ def fit(self, second_level_input, confounds=None, design_matrix=None): # sort a pandas dataframe by subject_label to avoid inconsistencies # with the design matrix row order when automatically extracting maps if isinstance(second_level_input, pd.DataFrame): - # Avoid pandas df.sort_value to keep compatibility with numpy 1.8 - # also pandas df.sort since it is completely deprecated. columns = second_level_input.columns.tolist() column_index = columns.index('subject_label') sorted_matrix = sorted( From e349bd91c5919b9a010354ad220936d1523ac267 Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Fri, 31 Aug 2018 09:57:18 +0200 Subject: [PATCH 041/210] Updated 'What's New' --- doc/whats_new.rst | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/doc/whats_new.rst b/doc/whats_new.rst index 17cb6969..fa8df58d 100644 --- a/doc/whats_new.rst +++ b/doc/whats_new.rst @@ -4,6 +4,22 @@ Changelog --------- +Updated the minimum versions of the dependencies + +* Numpy >= 1.11 +* SciPy >= 0.17 +* Nibabel >= 2.0.2 +* Nilearn >= 0.4.0 +* Pandas >= 0.18.0 +* Sklearn >= 0.18.0 + + +0.0.1a +======= + +Changelog +--------- + First alpha release of nistats. Contributors (from ``git shortlog -ns``):: From 11e89d704956cafd585137748d9ddcf4f9457392 Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Fri, 31 Aug 2018 09:58:59 +0200 Subject: [PATCH 042/210] Updated release version number in 'What's New' --- doc/whats_new.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/whats_new.rst b/doc/whats_new.rst index fa8df58d..61943e9b 100644 --- a/doc/whats_new.rst +++ b/doc/whats_new.rst @@ -1,4 +1,4 @@ -0.0.1a +0.0.2a ======= Changelog From 404dc08ff1ce94fede4a9c777af3c51a521e58dc Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Fri, 31 Aug 2018 10:14:56 +0200 Subject: [PATCH 043/210] Removed unnecessary try block after numpy v>=1.00 --- nistats/tests/test_model.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/nistats/tests/test_model.py b/nistats/tests/test_model.py index bf1ff7e2..d08aa3ea 100644 --- a/nistats/tests/test_model.py +++ b/nistats/tests/test_model.py @@ -59,11 +59,7 @@ def test_model(): assert_array_almost_equal(RESULTS.theta[1], np.mean(Y)) # Check we get the same as R assert_array_almost_equal(RESULTS.theta, [1.773, 2.5], 3) - try: - percentile = np.percentile - except AttributeError: - # Numpy <=1.4.1 does not have percentile function - raise SkipTest('Numpy does not have percentile function') + percentile = np.percentile pcts = percentile(RESULTS.resid, [0, 25, 50, 75, 100]) assert_array_almost_equal(pcts, [-1.6970, -0.6667, 0, 0.6667, 1.6970], 4) From ba03bab33c241777278847faf6550bb849e3ad8b Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Fri, 31 Aug 2018 11:16:49 +0200 Subject: [PATCH 044/210] Removed CircleCI 1.0 config file after updgrading to CircleCI 2.0 --- circle_old.yml | 51 -------------------------------------------------- 1 file changed, 51 deletions(-) delete mode 100644 circle_old.yml diff --git a/circle_old.yml b/circle_old.yml deleted file mode 100644 index a71789f7..00000000 --- a/circle_old.yml +++ /dev/null @@ -1,51 +0,0 @@ -machine: - environment: - PATH: /home/ubuntu/miniconda3/bin:$PATH - -dependencies: - cache_directories: - - "~/nilearn_data" - - pre: - # Get rid of existing virtualenvs on circle ci as they conflict with conda. - # Trick found here: - # https://discuss.circleci.com/t/disable-autodetection-of-project-or-application-of-python-venv/235/10 - - cd && rm -rf ~/.pyenv && rm -rf ~/virtualenvs - # We need to remove conflicting texlive packages. - - sudo -E apt-get -yq remove texlive-binaries --purge - # Installing required packages for `make -C doc check command` to work. - - sudo -E apt-get -yq update - - sudo -E apt-get -yq --no-install-suggests --no-install-recommends --force-yes install dvipng texlive-latex-base texlive-latex-extra - - override: - # Moving to nistats directory before performing the installation. - - cd ~/nistats - - source continuous_integration/install.sh: - environment: - DISTRIB: "conda" - PYTHON_VERSION: "3.5" - NUMPY_VERSION: "*" - SCIPY_VERSION: "*" - SCIKIT_LEARN_VERSION: "*" - MATPLOTLIB_VERSION: "*" - PANDAS_VERSION: "*" - - conda install sphinx coverage pillow -y -n testenv - - # Generating html documentation (with warnings as errors) - # we need to do this here so the datasets will be cached - - source continuous_integration/circle_ci_test_doc.sh: - timeout: 1500 - -test: - override: - - echo "ignore unit tests in circleCI" - # - make clean test test-coverage - # workaround - make html returns 0 even if examples fail to build - # (see https://github.com/sphinx-gallery/sphinx-gallery/issues/45) - - cat ~/log.txt && if grep -q "Traceback (most recent call last):" ~/log.txt; then false; else true; fi - -general: - artifacts: - - "doc/_build/html" - - "coverage" - - "~/log.txt" From 4607c04a2f7552d8b5540ed5b12b9ab3d312cd70 Mon Sep 17 00:00:00 2001 From: bthirion Date: Fri, 31 Aug 2018 13:51:27 +0200 Subject: [PATCH 045/210] Added a second-level two sample test example --- ...y => plot_second_level_one_sample_test.py} | 0 .../plot_second_level_two_sample_test.py | 75 +++++++++++++++++++ 2 files changed, 75 insertions(+) rename examples/03_second_level_models/{plot_second_level_button_press.py => plot_second_level_one_sample_test.py} (100%) create mode 100644 examples/03_second_level_models/plot_second_level_two_sample_test.py diff --git a/examples/03_second_level_models/plot_second_level_button_press.py b/examples/03_second_level_models/plot_second_level_one_sample_test.py similarity index 100% rename from examples/03_second_level_models/plot_second_level_button_press.py rename to examples/03_second_level_models/plot_second_level_one_sample_test.py diff --git a/examples/03_second_level_models/plot_second_level_two_sample_test.py b/examples/03_second_level_models/plot_second_level_two_sample_test.py new file mode 100644 index 00000000..8bbe88cc --- /dev/null +++ b/examples/03_second_level_models/plot_second_level_two_sample_test.py @@ -0,0 +1,75 @@ +""" +GLM fitting in second level fMRI +================================ + +Full step-by-step example of fitting a GLM to perform a second level analysis +in experimental data and visualizing the results. + +More specifically: + +1. A sequence of subject fMRI button press images is downloaded. +2. A two-sample t-test is applied to the brain maps +to see the effect of the contrast difference across subjects. +""" + +import pandas as pd +from nilearn import plotting +from nilearn.datasets import fetch_localizer_contrasts + +######################################################################### +# Fetch dataset +# -------------- +# We download a list of left vs right button press contrasts from a +# localizer dataset. +n_subjects = 16 +sample_vertical = fetch_localizer_contrasts( + ["vertical checkerboard"], n_subjects, get_tmaps=True) +sample_horizontal = fetch_localizer_contrasts( + ["horizontal checkerboard"], n_subjects, get_tmaps=True) + +# What remains implicit here is that there is a one-to-one +# correspondence between the two sample: the first image of both +# samples comes from subject S1, the second from subject S2 etc. + +############################################################################ +# Estimate second level model +# --------------------------- +# We define the input maps and the design matrix for the second level model +# and fit it. +second_level_input = sample_vertical['cmaps'] + sample_horizontal['cmaps'] + +# model the effect of conditions (sample 1 vs sample 2) +import numpy as np +condition_effect = np.hstack(([1] * n_subjects, [- 1] * n_subjects)) + +# model the subject effect: each subject is observed in sample 1 and sample 2 +subject_effect = np.vstack((np.eye(n_subjects), np.eye(n_subjects))) +subjects = ['S%02d' % i for i in range(1, n_subjects + 1)] +design_matrix = pd.DataFrame( + np.hstack((condition_effect[:, np.newaxis], subject_effect)), + columns=['vertical vs horizontal'] + subjects) + +# plot the design_matrix +from nistats.reporting import plot_design_matrix +plot_design_matrix(design_matrix) + +# formally specify the analysis model and fit it +from nistats.second_level_model import SecondLevelModel +second_level_model = SecondLevelModel() +second_level_model = second_level_model.fit( + second_level_input, design_matrix=design_matrix) + +########################################################################## +# To estimate the contrast is very simple. We can just provide the column +# name of the design matrix. +z_map = second_level_model.compute_contrast('vertical vs horizontal', + output_type='z_score') + +########################################################################### +# We threshold the second level contrast and plot it +threshold = 3.1 # correponds to p < .001, uncorrected +display = plotting.plot_glass_brain( + z_map, threshold=threshold, colorbar=True, plot_abs=False, + title='vertical vs horizontal checkerboard (unc p<0.001') + +plotting.show() From f038342ea9717bd092f0b10ab1c19a1d90be1e40 Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Mon, 3 Sep 2018 16:29:25 +0200 Subject: [PATCH 046/210] Variable value is correctly interpolated into the warning text --- nistats/hemodynamic_models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nistats/hemodynamic_models.py b/nistats/hemodynamic_models.py index 55842a45..a7da6057 100644 --- a/nistats/hemodynamic_models.py +++ b/nistats/hemodynamic_models.py @@ -270,7 +270,7 @@ def _sample_condition(exp_condition, frame_times, oversampling=16, # Get the condition information onsets, durations, values = tuple(map(np.asanyarray, exp_condition)) if (onsets < frame_times[0] + min_onset).any(): - warnings.warn(('Some stimulus onsets are earlier than %d in the' + + warnings.warn(('Some stimulus onsets are earlier than %s in the' ' experiment and are thus not considered in the model' % (frame_times[0] + min_onset)), UserWarning) From da4897b6283a6217960706a77f9edecc57548b2e Mon Sep 17 00:00:00 2001 From: bthirion Date: Wed, 5 Sep 2018 14:24:58 +0200 Subject: [PATCH 047/210] exposed oversampling parameter --- nistats/design_matrix.py | 20 ++++++++++++++++---- nistats/tests/test_dmtx.py | 23 +++++++++++++++++++++++ 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/nistats/design_matrix.py b/nistats/design_matrix.py index f5fdba73..53137e8d 100644 --- a/nistats/design_matrix.py +++ b/nistats/design_matrix.py @@ -164,7 +164,7 @@ def _make_drift(drift_model, frame_times, order=1, period_cut=128.): def _convolve_regressors(paradigm, hrf_model, frame_times, fir_delays=[0], - min_onset=-24): + min_onset=-24, oversampling=None): """ Creation of a matrix that comprises the convolution of the conditions onset with a certain hrf model @@ -191,6 +191,10 @@ def _convolve_regressors(paradigm, hrf_model, frame_times, fir_delays=[0], Minimal onset relative to frame_times[0] (in seconds) events that start before frame_times[0] + min_onset are not considered. + oversampling: float or None, optional, + Oversampling factor used in temporal convolutions. Should be 1 for + whenever hrf_mode is 'fir' and 16 otherwise. + Returns ------- regressor_matrix : array of shape (n_scans, n_regressors), @@ -210,8 +214,11 @@ def _convolve_regressors(paradigm, hrf_model, frame_times, fir_delays=[0], regressor_names = [] regressor_matrix = None if hrf_model == 'fir': + if oversampling not in [1, None]: + warn('Forcing oversampling factor to 1 for a finite' + 'impulse response hrf model') oversampling = 1 - else: + elif oversampling is None: oversampling = 16 trial_type, onset, duration, modulation = check_paradigm(paradigm) @@ -274,7 +281,7 @@ def _full_rank(X, cmax=1e15): def make_design_matrix( frame_times, paradigm=None, hrf_model='glover', drift_model='cosine', period_cut=128, drift_order=1, fir_delays=[0], - add_regs=None, add_reg_names=None, min_onset=-24): + add_regs=None, add_reg_names=None, min_onset=-24, oversampling=None): """Generate a design matrix from the input parameters Parameters @@ -332,6 +339,10 @@ def make_design_matrix( Minimal onset relative to frame_times[0] (in seconds) events that start before frame_times[0] + min_onset are not considered. + oversampling: float or None, optional, + Oversampling factor used in temporal convolutions. Should be 1 for + whenever hrf_mode is 'fir' and 16 otherwise. + Returns ------- design_matrix : DataFrame instance, @@ -369,7 +380,8 @@ def make_design_matrix( if isinstance(hrf_model, _basestring): hrf_model = hrf_model.lower() matrix, names = _convolve_regressors( - paradigm, hrf_model, frame_times, fir_delays, min_onset) + paradigm, hrf_model, frame_times, fir_delays, min_onset, + oversampling) # step 2: additional regressors if add_regs is not None: diff --git a/nistats/tests/test_dmtx.py b/nistats/tests/test_dmtx.py index 1d067173..2b0b76a7 100644 --- a/nistats/tests/test_dmtx.py +++ b/nistats/tests/test_dmtx.py @@ -421,6 +421,29 @@ def test_fir_block(): assert_true((X[idx + 2, 6] == 1).all()) assert_true((X[idx + 3, 7] == 1).all()) +def test_oversampling(): + paradigm = basic_paradigm() + frame_times = np.linspace(0, 127, 128) + X1 = make_design_matrix( + frame_times, paradigm, drift_model=None) + X2 = make_design_matrix( + frame_times, paradigm, drift_model=None, oversampling=16) + X3 = make_design_matrix( + frame_times, paradigm, drift_model=None, oversampling=10) + + # oversampling = 16 is the default so X2 = X1, X3 \neq X1, X3 close to X2 + assert_almost_equal(X1.values, X2.values) + assert_almost_equal(X2.values, X3.values, 0) + assert_true(np.linalg.norm(X2.values - X3.values) / np.linalg.norm(X2.values) > 1.e-4) + + # fir model, oversampling is forced to 1 + X4 = make_design_matrix( + frame_times, paradigm, hrf_model='fir', drift_model=None, + fir_delays=range(0, 4), oversampling=1) + X5 = make_design_matrix( + frame_times, paradigm, hrf_model='fir', drift_model=None, + fir_delays=range(0, 4), oversampling=3) + assert_almost_equal(X4.values, X5.values) def test_csv_io(): # test the csv io on design matrices From 0da21a4fc43d0f141fc1d9d4132be3482e89601b Mon Sep 17 00:00:00 2001 From: chrplr Date: Fri, 7 Sep 2018 17:52:29 +0200 Subject: [PATCH 048/210] typos corrected in intro --- doc/user_guide.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/user_guide.rst b/doc/user_guide.rst index aca2549f..ccd62904 100644 --- a/doc/user_guide.rst +++ b/doc/user_guide.rst @@ -19,5 +19,5 @@ User guide: table of contents :numbered: introduction.rst - modules/reference.rst auto_examples/index.rst + modules/reference.rst From 3ed2c3c03ac91fe0b63765d7adc528225daeab39 Mon Sep 17 00:00:00 2001 From: bthirion Date: Fri, 7 Sep 2018 22:15:59 +0200 Subject: [PATCH 049/210] updated introduction --- doc/introduction.rst | 5 +- .../01_tutorials/single_subject_single_run.py | 193 ++++++++++++++++++ 2 files changed, 196 insertions(+), 2 deletions(-) create mode 100644 examples/01_tutorials/single_subject_single_run.py diff --git a/doc/introduction.rst b/doc/introduction.rst index 0b8cd701..d1fbcc45 100644 --- a/doc/introduction.rst +++ b/doc/introduction.rst @@ -33,7 +33,7 @@ A cerebral MR image provides a 3D image of the brain that can be decomposed into .. _a voxels: https://en.wikipedia.org/wiki/Voxel -TODO: INSERT HERE A SAMPLE OF A TIME SERIES IN A VOXEL (or several voxels) +.. figure:: images/stimulation-time-diagram.png .. note:: A typical step in the preprocessing of MR images, involves spatially morphing these images onto a standard template (e.g. the MNI152 template from the Montreal Neurological Institute). One this is done, the coordinates of a voxel are in the same space as the template and can be used to estimate its brain location using brain atlases. As already mentioned, the nistats package is not meant to perform spiatal preprocessing, but only statistical analyses on the voxel times series, regardless of the coordinate system. @@ -43,7 +43,8 @@ One way to analyze times series consists in comparing them to a *model* built fr .. figure:: images/stimulation-time-diagram.png -One expects that a brain region involved in the processing of a certain type of event (e.g. the auditory cortex for sounds), would show a time course of activation that correlates with the time-diagram of these events. If the fMRI signal directly showed neural activity and did not contain any noise, we could just look at it in various voxel and detect those that conform to the time-diagrams. +One expects that a brain region involved in the processing of a certain type of event (e.g. the auditory cortex for sounds), would show a time course of activation that correlates with the time-diagram of these events. If the fMRI signal directly showed neural activity and did not contain any noise, we could just look at it in various voxel and d +etect those that conform to the time-diagrams. Yet, we know, from previous measurements, that the BOLD signal does not follow the exact time course of stimulus processing and the underlying neural activity. The BOLD response reflects changes in blood flow and concentrations in oxy-deoxy hemoglobin, all together forming an `a haemodynamic response`_ which is sluggish and long-lasting, as can be seen on the following figurte showing the response to an impulsive event (for example, an auditory click played to the participants). diff --git a/examples/01_tutorials/single_subject_single_run.py b/examples/01_tutorials/single_subject_single_run.py new file mode 100644 index 00000000..05c728d2 --- /dev/null +++ b/examples/01_tutorials/single_subject_single_run.py @@ -0,0 +1,193 @@ +""" +Analysis of a block design (stimulation vs rest), single session, single subject. +================================================================================= + +In this tutorial, we compare the fMRI signal during periods of auditory +stimulation versus periods of rest, using a General Linear Model (GLM). + +The dataset comes from an experiment conducted at the FIL by Geriant Rees +under the direction of Karl Friston. It is provided by FIL methods +group which develops the SPM software. + +According to SPM documentation, 96 scans were acquired (RT=7s) in one session. THe paradigm consisted of alternating periods of stimulation and rest, lasting 42s each (that is, for 6 scans). The sesssion started with a rest block. +Auditory stimulation consisted of bi-syllabic words presented binaurally at a +rate of 60 per minute. The functional data starts at scan number 4, that is the +image file ``fM00223_004``. + +The whole brain BOLD/EPI images were acquired on a 2T Siemens +MAGNETOM Vision system. Each scan consisted of 64 contiguous +slices (64x64x64 3mm x 3mm x 3mm voxels). Acquisition of one scan took 6.05s, with the scan to scan repeat time (RT) set arbitrarily to 7s. + +The analyse described here is performed in the native space, directly on the +original EPI scans without any spatial or temporal preprocessing. +(More sensitive results would likely be obtained on the corrected, +spatially normalized and smoothed images). + + +To run this example, you must launch IPython via ``ipython +--matplotlib`` in a terminal, or use ``jupyter-notebook``. + +.. contents:: **Contents** + :local: + :depth: 1 + +""" + +import matplotlib.pyplot as plt + +############################################################################### +# Retrieving the data +# ------------------- +# +# .. note:: In this tutorial, we load the data using a data downloading +# function. To input your own data, you will need to provide +# a list of paths to your own files in the ``subject_data`` variable. + +from nistats.datasets import fetch_spm_auditory +subject_data = fetch_spm_auditory() +print(subject_data.func) # print the list of names of functional images + +############################################################################### +# We can display the first functional image and the subject's anatomy: +from nilearn.plotting import plot_stat_map, plot_anat, plot_img +plot_img(subject_data.func[0]) +plot_anat(subject_data.anat) + +############################################################################### +# Next, we concatenate all the 3D EPI image into a single 4D image, +# the we average them in order to create a background +# image that will be used to display the activations: + +from nilearn.image import concat_imgs, mean_img +fmri_img = concat_imgs(subject_data.func) +mean_img = mean_img(fmri_img) + +############################################################################### +# Specifying the experimental paradigm +# ------------------------------------ +# +# We must provide now a description of the experiment, that is, define the +# timing of the auditory stimulation and rest periods. According to +# the documentation of the dataset, there were sixteen 42s-long blocks --- in +# which 6 scans were acquired --- alternating between rest and +# auditory stimulation, starting with rest. +# +# The following table provide all the relevant informations: +# + +""" +duration, onset, trial_type + 42 , 0 , rest + 42 , 42 , active + 42 , 84 , rest + 42 , 126 , active + 42 , 168 , rest + 42 , 210 , active + 42 , 252 , rest + 42 , 294 , active + 42 , 336 , rest + 42 , 378 , active + 42 , 420 , rest + 42 , 462 , active + 42 , 504 , rest + 42 , 546 , active + 42 , 588 , rest + 42 , 630 , active +""" + +# We can read such a table from a spreadsheet file created with OpenOffice Calcor Office Excel, and saved under the *comma separated values* format (``.csv``). + +import pandas as pd +events = pd.read_csv('auditory_block_paradigm.csv') +print(events) + +# ## ################################################################### +# # Alternatively, we could have used standard python +# # functions to create a pandas.DataFrame object that specifies the +# # timings: + + +############################################################################### +# Performing the GLM analysis +# --------------------------- +# +# It is now time to create and estimate a ``FirstLevelModel`` object, which will# generate the *design matrix* using the information provided by the ``events` object. + +from nistats.first_level_model import FirstLevelModel + +fmri_glm = FirstLevelModel(t_r=7, + noise_model='ar1', + standardize=False, + hrf_model='spm', + drift_model='cosine', + period_cut=160) +fmri_glm = fmri_glm.fit(fmri_img, events) + +############################################################################### +# One can inspect the design matrix (rows represent time, and +# columns contain the predictors): + +from nistats.reporting import plot_design_matrix +design_matrix = fmri_glm.design_matrices_[0] +plot_design_matrix(design_matrix) +plt.show() + +############################################################################### +# The first column contains the expected reponse profile of regions which are +# sensitive to the auditory stimulation. + +plt.plot(design_matrix['active']) +plt.xlabel('scan') +plt.title('Expected Auditory Response') +plt.show() + + +############################################################################### +# Detecting voxels with significant effects +# ----------------------------------------- +# +# To access the estimated coefficients (Betas of the GLM model), we +# created constrasts with a single '1' in each of the columns: +# TODO: simplify!!! + + + +# contrasts:: +# TODO explain why contrasts are specified in such a way. Contrasts as weighted sum... use an example .... + conditions = { + 'active': array([ 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]), + 'rest': array([ 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0.]), +} +############################################################################### +# We can then compare the two conditions 'active' and 'rest' by +# generating the relevant contrast: + +active_minus_rest = conditions['active'] - conditions['rest'] + +eff_map = fmri_glm.compute_contrast(active_minus_rest, + output_type='effect_size') + +z_map = fmri_glm.compute_contrast(active_minus_rest, + output_type='z_score') + +############################################################################### +# Plot thresholded z scores map + +plot_stat_map(z_map, bg_img=mean_img, threshold=3.0, + display_mode='z', cut_coords=3, black_bg=True, + title='Active minus Rest (Z>3)') +plt.show() + +############################################################################### +# We can use ``nibabel.save`` to save the effect and zscore maps to the disk + +import os +outdir = 'results' +if not os.path.exists(outdir): + os.mkdir(outdir) + +import nibabel +from os.path import join +nibabel.save(z_map, join('results', 'active_vs_rest_z_map.nii')) +nibabel.save(eff_map, join('results', 'active_vs_rest_eff_map.nii')) + From 7372a3b744072606014301d2f7806417b0c68abe Mon Sep 17 00:00:00 2001 From: bthirion Date: Fri, 7 Sep 2018 22:17:14 +0200 Subject: [PATCH 050/210] added figure --- doc/images/time_courses.png | Bin 0 -> 46140 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 doc/images/time_courses.png diff --git a/doc/images/time_courses.png b/doc/images/time_courses.png new file mode 100644 index 0000000000000000000000000000000000000000..1180e1904b1e7cd65080b6eab32ed3c3f52da478 GIT binary patch literal 46140 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

m&bOLAn&n?iRl3F;-b&b3gyhCUaS$ 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 F{{yC|zj*)v literal 0 HcmV?d00001 From 05f6b4ea28dde38e730102aaf3e5b449e5451f44 Mon Sep 17 00:00:00 2001 From: bthirion Date: Fri, 7 Sep 2018 22:18:18 +0200 Subject: [PATCH 051/210] removed extracting script that blocks compilation --- .../extracting-signal-from-a-voxel.py | 38 ------------------- 1 file changed, 38 deletions(-) delete mode 100644 examples/01_tutorials/extracting-signal-from-a-voxel.py diff --git a/examples/01_tutorials/extracting-signal-from-a-voxel.py b/examples/01_tutorials/extracting-signal-from-a-voxel.py deleted file mode 100644 index b841c4c5..00000000 --- a/examples/01_tutorials/extracting-signal-from-a-voxel.py +++ /dev/null @@ -1,38 +0,0 @@ -#! /usr/bin/env python -# TODO -# This follows the singlesubject/single (spm_auditory) session example - -############################################################################### -# Extract the signal from a voxel -# ------------------------------- -# -# We search for the voxel with the larger z-score and plot the signal -# (warning: this is "double dipping") - - -# Find the coordinates of the peak - -from nibabel.affines import apply_affine -values = z_map.get_data() -coord_peaks = np.dstack(np.unravel_index(np.argsort(-values.ravel()), - values.shape))[0, 0, :] -coord_mm = apply_affine(z_map.affine, coord_peaks) - -############################################################################### -# We create a masker for the voxel (allowing us to detrend the signal) -# and extract the time course - -from nilearn.input_data import NiftiSpheresMasker -mask = NiftiSpheresMasker([coord_mm], radius=3, - detrend=True, standardize=True, - high_pass=None, low_pass=None, t_r=7.) -sig = mask.fit_transform(fmri_img) - -########################################################## -# Let's plot the signal and the theoretical response - -plt.plot(frame_times, sig, label='voxel %d %d %d' % tuple(coord_mm)) -plt.plot(design_matrix['active'], color='red', label='model') -plt.xlabel('scan') -plt.legend() -plt.show() From 27740fbd660862fb8ee62684daa4ebd0e71885fc Mon Sep 17 00:00:00 2001 From: bthirion Date: Sat, 8 Sep 2018 17:41:34 +0200 Subject: [PATCH 052/210] Completed the single_run script with more explanations --- .../01_tutorials/single_subject_single_run.py | 64 +++++++++++-------- 1 file changed, 37 insertions(+), 27 deletions(-) diff --git a/examples/01_tutorials/single_subject_single_run.py b/examples/01_tutorials/single_subject_single_run.py index 05c728d2..e0cefbae 100644 --- a/examples/01_tutorials/single_subject_single_run.py +++ b/examples/01_tutorials/single_subject_single_run.py @@ -9,14 +9,14 @@ under the direction of Karl Friston. It is provided by FIL methods group which develops the SPM software. -According to SPM documentation, 96 scans were acquired (RT=7s) in one session. THe paradigm consisted of alternating periods of stimulation and rest, lasting 42s each (that is, for 6 scans). The sesssion started with a rest block. +According to SPM documentation, 96 scans were acquired (repetition time TR=7s) in one session. The paradigm consisted of alternating periods of stimulation and rest, lasting 42s each (that is, for 6 scans). The sesssion started with a rest block. Auditory stimulation consisted of bi-syllabic words presented binaurally at a rate of 60 per minute. The functional data starts at scan number 4, that is the image file ``fM00223_004``. The whole brain BOLD/EPI images were acquired on a 2T Siemens MAGNETOM Vision system. Each scan consisted of 64 contiguous -slices (64x64x64 3mm x 3mm x 3mm voxels). Acquisition of one scan took 6.05s, with the scan to scan repeat time (RT) set arbitrarily to 7s. +slices (64x64x64 3mm x 3mm x 3mm voxels). Acquisition of one scan took 6.05s, with the scan to scan repeat time (TR) set arbitrarily to 7s. The analyse described here is performed in the native space, directly on the original EPI scans without any spatial or temporal preprocessing. @@ -45,8 +45,8 @@ from nistats.datasets import fetch_spm_auditory subject_data = fetch_spm_auditory() -print(subject_data.func) # print the list of names of functional images - +print(subject_data.func) # print the list of names of functional images + ############################################################################### # We can display the first functional image and the subject's anatomy: from nilearn.plotting import plot_stat_map, plot_anat, plot_img @@ -54,7 +54,7 @@ plot_anat(subject_data.anat) ############################################################################### -# Next, we concatenate all the 3D EPI image into a single 4D image, +# Next, we concatenate all the 3D EPI image into a single 4D image, # the we average them in order to create a background # image that will be used to display the activations: @@ -72,7 +72,7 @@ # which 6 scans were acquired --- alternating between rest and # auditory stimulation, starting with rest. # -# The following table provide all the relevant informations: +# The following table provide all the relevant informations: # """ @@ -101,12 +101,6 @@ events = pd.read_csv('auditory_block_paradigm.csv') print(events) -# ## ################################################################### -# # Alternatively, we could have used standard python -# # functions to create a pandas.DataFrame object that specifies the -# # timings: - - ############################################################################### # Performing the GLM analysis # --------------------------- @@ -115,12 +109,24 @@ from nistats.first_level_model import FirstLevelModel +# t_r=7(s) is the time of repetition of acquisitions +# noise_model='ar1' specifies the noise covariance model: a lag-1 dependence +# standardize=False means that we do not want to rescale the time +# series to mean 0, variance 1 +# hrf_model='spm' means that we rely on the SPM "canonical hrf" model +# (without time or dispersion derivatives) +# drift_model='cosine' means that we model the signal drifts as slow +# oscillating time functions +# periodècut=160(s) defines the cutoff frequency (its inverse actually). + fmri_glm = FirstLevelModel(t_r=7, noise_model='ar1', standardize=False, hrf_model='spm', drift_model='cosine', period_cut=160) + +# Now that we have specified the mdoel, we can run it on the fMRI image fmri_glm = fmri_glm.fit(fmri_img, events) ############################################################################### @@ -129,6 +135,8 @@ from nistats.reporting import plot_design_matrix design_matrix = fmri_glm.design_matrices_[0] +# We have taken the first design matrix, because the model is meant +# for multiple runs plot_design_matrix(design_matrix) plt.show() @@ -136,42 +144,45 @@ # The first column contains the expected reponse profile of regions which are # sensitive to the auditory stimulation. +# Let's plot this first column plt.plot(design_matrix['active']) plt.xlabel('scan') plt.title('Expected Auditory Response') plt.show() - ############################################################################### # Detecting voxels with significant effects # ----------------------------------------- # # To access the estimated coefficients (Betas of the GLM model), we -# created constrasts with a single '1' in each of the columns: -# TODO: simplify!!! - - +# created constrast with a single '1' in each of the columns: The role of the contrast is to select some columns of the model --and potentially weight them-- to study the associated statistics. So in a nutshell, a contrast is a linear combination of the estimated effects +# Here we can define canonical contrasts that just consider the two condition in isolation, let's call them "conditions", then a contrast that makes the difference between these conditions. -# contrasts:: -# TODO explain why contrasts are specified in such a way. Contrasts as weighted sum... use an example .... - conditions = { - 'active': array([ 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]), - 'rest': array([ 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0.]), +from numpy import array +conditions = { + 'active': array([1., 0., 0., 0., 0., 0., 0., 0., 0., 0.]), + 'rest': array([0., 1., 0., 0., 0., 0., 0., 0., 0., 0.]), } + ############################################################################### # We can then compare the two conditions 'active' and 'rest' by # generating the relevant contrast: active_minus_rest = conditions['active'] - conditions['rest'] +# this is the estimated effect. It is in BOLD signal unit, but has no statistical guarantees, because it does not take into account the associated variance eff_map = fmri_glm.compute_contrast(active_minus_rest, output_type='effect_size') +# In order to get statistical significance, we form a t-statistic, and directly convert is into z-scale. z_map = fmri_glm.compute_contrast(active_minus_rest, output_type='z_score') ############################################################################### # Plot thresholded z scores map +# we display it on top of the average functional image of the seris (could be the anatomical image of the subject). +# we use arbitrarily a threshold of 3.0 in z-scale. We'll see later how to use corrected thresholds. +# we show to display 3 axial views: display_mode='z', cut_coords=3 plot_stat_map(z_map, bg_img=mean_img, threshold=3.0, display_mode='z', cut_coords=3, black_bg=True, @@ -179,15 +190,14 @@ plt.show() ############################################################################### -# We can use ``nibabel.save`` to save the effect and zscore maps to the disk +# We can save the effect and zscore maps to the disk +# first create a directory where you want tow rite the images import os outdir = 'results' if not os.path.exists(outdir): os.mkdir(outdir) -import nibabel from os.path import join -nibabel.save(z_map, join('results', 'active_vs_rest_z_map.nii')) -nibabel.save(eff_map, join('results', 'active_vs_rest_eff_map.nii')) - +z_map.to_filename(join('results', 'active_vs_rest_z_map.nii.gz')) +eff_map.to_filename(join('results', 'active_vs_rest_eff_map.nii.gz')) From c77dd358e8d3efc5013f6315defbb589c7588843 Mon Sep 17 00:00:00 2001 From: bthirion Date: Sun, 9 Sep 2018 10:59:41 +0200 Subject: [PATCH 053/210] fixed doc geenration --- ...block_design_single_subject_single_bloc.py | 220 ------------------ ...n.py => plot_single_subject_single_run.py} | 28 ++- 2 files changed, 19 insertions(+), 229 deletions(-) delete mode 100644 examples/01_tutorials/block_design_single_subject_single_bloc.py rename examples/01_tutorials/{single_subject_single_run.py => plot_single_subject_single_run.py} (92%) diff --git a/examples/01_tutorials/block_design_single_subject_single_bloc.py b/examples/01_tutorials/block_design_single_subject_single_bloc.py deleted file mode 100644 index 678534cb..00000000 --- a/examples/01_tutorials/block_design_single_subject_single_bloc.py +++ /dev/null @@ -1,220 +0,0 @@ -""" -Analysis of a block design (stimulation vs rest), single session, single subject. -================================================================================= - -In this tutorial, we compare the fMRI signal during periods of auditory -stimulation versus periods of rest, using a General Linear Model (GLM). - -The dataset comes from an experiment conducted at the FIL by Geriant Rees -under the direction of Karl Friston. It is provided by FIL methods -group which develops the SPM software. - -According to SPM documentation, 96 scans were acquired (RT=7s) in one session. THe paradigm consisted of alternating periods of stimulation and rest, lasting 42s each (that is, for 6 scans). The sesssion started with a rest block. -Auditory stimulation consisted of bi-syllabic words presented binaurally at a -rate of 60 per minute. The functional data starts at scan number 4, that is the -image file ``fM00223_004``. - -The whole brain BOLD/EPI images were acquired on a 2T Siemens -MAGNETOM Vision system. Each scan consisted of 64 contiguous -slices (64x64x64 3mm x 3mm x 3mm voxels). Acquisition of one scan took 6.05s, with the scan to scan repeat time (RT) set arbitrarily to 7s. - -The analyse described here is performed in the native space, directly on the -original EPI scans without any spatial or temporal preprocessing. -(More sensitive results would likely be obtained on the corrected, -spatially normalized and smoothed images). - - -To run this example, you must launch IPython via ``ipython ---matplotlib`` in a terminal, or use ``jupyter-notebook``. - -.. contents:: **Contents** - :local: - :depth: 1 - -""" - -import matplotlib.pyplot as plt - -############################################################################### -# Retrieving the data -# ------------------- -# -# .. note:: In this tutorial, we load the data using a data downloading -# function. To input your own data, you will need to provide -# a list of paths to your own files in the ``subject_data`` variable. - -from nistats.datasets import fetch_spm_auditory -subject_data = fetch_spm_auditory() -print(subject_data.func) # print the list of names of functional images - -############################################################################### -# We can display the first functional image and the subject's anatomy: -from nilearn.plotting import plot_stat_map, plot_anat, plot_img -plot_img(subject_data.func[0]) -plot_anat(subject_data.anat) - -############################################################################### -# Next, we concatenate all the 3D EPI image into a single 4D image, -# the we average them in order to create a background -# image that will be used to display the activations: - -from nilearn.image import concat_imgs, mean_img -fmri_img = concat_imgs(subject_data.func) -mean_img = mean_img(fmri_img) - -############################################################################### -# Specifying the experimental paradigm -# ------------------------------------ -# -# We must provide now a description of the experiment, that is, define the -# timing of the auditory stimulation and rest periods. According to -# the documentation of the dataset, there were sixteen 42s-long blocks --- in -# which 6 scans were acquired --- alternating between rest and -# auditory stimulation, starting with rest. -# -# The following table provide all the relevant informations: -# - -""" -duration, onset, trial_type - 42 , 0 , rest - 42 , 42 , active - 42 , 84 , rest - 42 , 126 , active - 42 , 168 , rest - 42 , 210 , active - 42 , 252 , rest - 42 , 294 , active - 42 , 336 , rest - 42 , 378 , active - 42 , 420 , rest - 42 , 462 , active - 42 , 504 , rest - 42 , 546 , active - 42 , 588 , rest - 42 , 630 , active -""" - -# We can read such a table from a spreadsheet file created with OpenOffice Calcor Office Excel, and saved under the *comma separated values* format (``.csv``). - -import pandas as pd -events = pd.read_csv('auditory_block_paradigm.csv') -print(events) - -# ## ################################################################### -# # Alternatively, we could have used standard python -# # functions to create a pandas.DataFrame object that specifies the -# # timings: - -# import numpy as np -# tr = 7. -# slice_time_ref = 0. -# n_scans = 96 -# epoch_duration = 6 * tr # duration in seconds -# conditions = ['rest', 'active'] * 8 -# n_blocks = len(conditions) -# duration = epoch_duration * np.ones(n_blocks) -# onset = np.linspace(0, (n_blocks - 1) * epoch_duration, n_blocks) -# events = pd.DataFrame( -# {'onset': onset, 'duration': duration, 'trial_type': conditions}) - - -############################################################################### -# Performing the GLM analysis -# --------------------------- -# -# It is now time to create and estimate a ``FirstLevelModel`` object, which will# generate the *design matrix* using the information provided by the ``events` object. - -from nistats.first_level_model import FirstLevelModel - -fmri_glm = FirstLevelModel(t_r=7, - noise_model='ar1', - standardize=False, - hrf_model='spm', - drift_model='cosine', - period_cut=160) -fmri_glm = fmri_glm.fit(fmri_img, events) - -############################################################################### -# One can inspect the design matrix (rows represent time, and -# columns contain the predictors): - -from nistats.reporting import plot_design_matrix -design_matrix = fmri_glm.design_matrices_[0] -plot_design_matrix(design_matrix) -plt.show() - -############################################################################### -# The first column contains the expected reponse profile of regions which are -# sensitive to the auditory stimulation. - -plt.plot(design_matrix['active']) -plt.xlabel('scan') -plt.title('Expected Auditory Response') -plt.show() - - -############################################################################### -# Detecting voxels with significant effects -# ----------------------------------------- -# -# To access the estimated coefficients (Betas of the GLM model), we -# created constrasts with a single '1' in each of the columns: -# TODO: simplify!!! - -import numpy as np -contrast_matrix = np.eye(design_matrix.shape[1]) -contrasts = dict([(column, contrast_matrix[i]) - for i, column in enumerate(design_matrix.columns)]) - -""" -contrasts:: - - { - 'active': array([ 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]), - 'active_derivative': array([ 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]), - 'constant': array([ 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1.]), - 'drift_1': array([ 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0.]), - 'drift_2': array([ 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0.]), - 'drift_3': array([ 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0.]), - 'drift_4': array([ 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0.]), - 'drift_5': array([ 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0.]), - 'drift_6': array([ 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0.]), - 'drift_7': array([ 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0.]), - 'rest': array([ 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0.]), - 'rest_derivative': array([ 0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0.])} -""" - -############################################################################### -# We can then compare the two conditions 'active' and 'rest' by -# generating the relevant contrast: - -active_minus_rest = contrasts['active'] - contrasts['rest'] - -eff_map = fmri_glm.compute_contrast(active_minus_rest, - output_type='effect_size') - -z_map = fmri_glm.compute_contrast(active_minus_rest, - output_type='z_score') - -############################################################################### -# Plot thresholded z scores map - -plot_stat_map(z_map, bg_img=mean_img, threshold=3.0, - display_mode='z', cut_coords=3, black_bg=True, - title='Active minus Rest (Z>3)') -plt.show() - -############################################################################### -# We can use ``nibabel.save`` to save the effect and zscore maps to the disk - -import os -outdir = 'results' -if not os.path.exists(outdir): - os.mkdir(outdir) - -import nibabel -from os.path import join -nibabel.save(z_map, join('results', 'active_vs_rest_z_map.nii')) -nibabel.save(eff_map, join('results', 'active_vs_rest_eff_map.nii')) - diff --git a/examples/01_tutorials/single_subject_single_run.py b/examples/01_tutorials/plot_single_subject_single_run.py similarity index 92% rename from examples/01_tutorials/single_subject_single_run.py rename to examples/01_tutorials/plot_single_subject_single_run.py index e0cefbae..ed0dc2cb 100644 --- a/examples/01_tutorials/single_subject_single_run.py +++ b/examples/01_tutorials/plot_single_subject_single_run.py @@ -1,6 +1,6 @@ """ -Analysis of a block design (stimulation vs rest), single session, single subject. -================================================================================= +Analysis of a single session, single subject fMRI dataset +========================================================= In this tutorial, we compare the fMRI signal during periods of auditory stimulation versus periods of rest, using a General Linear Model (GLM). @@ -33,8 +33,6 @@ """ -import matplotlib.pyplot as plt - ############################################################################### # Retrieving the data # ------------------- @@ -49,7 +47,7 @@ ############################################################################### # We can display the first functional image and the subject's anatomy: -from nilearn.plotting import plot_stat_map, plot_anat, plot_img +from nilearn.plotting import plot_stat_map, plot_anat, plot_img, show plot_img(subject_data.func[0]) plot_anat(subject_data.anat) @@ -95,8 +93,7 @@ 42 , 630 , active """ -# We can read such a table from a spreadsheet file created with OpenOffice Calcor Office Excel, and saved under the *comma separated values* format (``.csv``). - +# We can read such a table from a spreadsheet file created with OpenOffice Calcor Office Excel, and saved under the *comma separated values* format (``.csv``). import pandas as pd events = pd.read_csv('auditory_block_paradigm.csv') print(events) @@ -109,6 +106,7 @@ from nistats.first_level_model import FirstLevelModel +############################################################################### # t_r=7(s) is the time of repetition of acquisitions # noise_model='ar1' specifies the noise covariance model: a lag-1 dependence # standardize=False means that we do not want to rescale the time @@ -126,6 +124,7 @@ drift_model='cosine', period_cut=160) +############################################################################### # Now that we have specified the mdoel, we can run it on the fMRI image fmri_glm = fmri_glm.fit(fmri_img, events) @@ -135,16 +134,20 @@ from nistats.reporting import plot_design_matrix design_matrix = fmri_glm.design_matrices_[0] + +############################################################################### # We have taken the first design matrix, because the model is meant # for multiple runs + +import matplotlib.pyplot as plt plot_design_matrix(design_matrix) plt.show() ############################################################################### # The first column contains the expected reponse profile of regions which are # sensitive to the auditory stimulation. - # Let's plot this first column + plt.plot(design_matrix['active']) plt.xlabel('scan') plt.title('Expected Auditory Response') @@ -170,11 +173,15 @@ active_minus_rest = conditions['active'] - conditions['rest'] +############################################################################### # this is the estimated effect. It is in BOLD signal unit, but has no statistical guarantees, because it does not take into account the associated variance + eff_map = fmri_glm.compute_contrast(active_minus_rest, output_type='effect_size') +############################################################################### # In order to get statistical significance, we form a t-statistic, and directly convert is into z-scale. + z_map = fmri_glm.compute_contrast(active_minus_rest, output_type='z_score') @@ -191,13 +198,16 @@ ############################################################################### # We can save the effect and zscore maps to the disk - # first create a directory where you want tow rite the images + import os outdir = 'results' if not os.path.exists(outdir): os.mkdir(outdir) +############################################################################### +# Then save the images in this directory + from os.path import join z_map.to_filename(join('results', 'active_vs_rest_z_map.nii.gz')) eff_map.to_filename(join('results', 'active_vs_rest_eff_map.nii.gz')) From eb73ce375fa04ffe52a41d3d48f97c855a86ed15 Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Wed, 12 Sep 2018 16:04:19 +0200 Subject: [PATCH 054/210] Removed the unnecessary "General Examples" section - Now getting a disappearing whitespace in its place. --- examples/README.txt | 5 ----- 1 file changed, 5 deletions(-) diff --git a/examples/README.txt b/examples/README.txt index 03e887c8..54853efa 100644 --- a/examples/README.txt +++ b/examples/README.txt @@ -12,8 +12,3 @@ Nistats usage examples .. contents:: **Contents** :local: :depth: 1 - -General examples ----------------- - -General-purpose and introductory examples for nistats. From 58bd0e9a1be87df48a675b02ee277e338bdef6a7 Mon Sep 17 00:00:00 2001 From: bthirion Date: Wed, 12 Sep 2018 17:06:23 +0200 Subject: [PATCH 055/210] Revert "Removed the unnecessary "General Examples" section" --- examples/README.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/examples/README.txt b/examples/README.txt index 54853efa..03e887c8 100644 --- a/examples/README.txt +++ b/examples/README.txt @@ -12,3 +12,8 @@ Nistats usage examples .. contents:: **Contents** :local: :depth: 1 + +General examples +---------------- + +General-purpose and introductory examples for nistats. From be8ccfaea6efbafdc4c255c615db18d1e577c7bb Mon Sep 17 00:00:00 2001 From: bthirion Date: Wed, 12 Sep 2018 19:55:06 +0200 Subject: [PATCH 056/210] Completed the example --- .../plot_single_subject_single_run.py | 77 +++++++++++++++---- 1 file changed, 60 insertions(+), 17 deletions(-) diff --git a/examples/01_tutorials/plot_single_subject_single_run.py b/examples/01_tutorials/plot_single_subject_single_run.py index ed0dc2cb..822c087f 100644 --- a/examples/01_tutorials/plot_single_subject_single_run.py +++ b/examples/01_tutorials/plot_single_subject_single_run.py @@ -64,7 +64,7 @@ # Specifying the experimental paradigm # ------------------------------------ # -# We must provide now a description of the experiment, that is, define the +# We must now provide a description of the experiment, that is, define the # timing of the auditory stimulation and rest periods. According to # the documentation of the dataset, there were sixteen 42s-long blocks --- in # which 6 scans were acquired --- alternating between rest and @@ -93,6 +93,7 @@ 42 , 630 , active """ +############################################################################### # We can read such a table from a spreadsheet file created with OpenOffice Calcor Office Excel, and saved under the *comma separated values* format (``.csv``). import pandas as pd events = pd.read_csv('auditory_block_paradigm.csv') @@ -102,20 +103,17 @@ # Performing the GLM analysis # --------------------------- # -# It is now time to create and estimate a ``FirstLevelModel`` object, which will# generate the *design matrix* using the information provided by the ``events` object. +# It is now time to create and estimate a ``FirstLevelModel`` object, that will generate the *design matrix* using the information provided by the ``events` object. from nistats.first_level_model import FirstLevelModel ############################################################################### # t_r=7(s) is the time of repetition of acquisitions -# noise_model='ar1' specifies the noise covariance model: a lag-1 dependence -# standardize=False means that we do not want to rescale the time -# series to mean 0, variance 1 -# hrf_model='spm' means that we rely on the SPM "canonical hrf" model -# (without time or dispersion derivatives) -# drift_model='cosine' means that we model the signal drifts as slow -# oscillating time functions -# periodècut=160(s) defines the cutoff frequency (its inverse actually). +# - noise_model='ar1' specifies the noise covariance model: a lag-1 dependence +# - standardize=False means that we do not want to rescale the time series to mean 0, variance 1 +# - hrf_model='spm' means that we rely on the SPM "canonical hrf" model (without time or dispersion derivatives) +# - drift_model='cosine' means that we model the signal drifts as slow oscillating time functions +# - periodècut=160(s) defines the cutoff frequency (its inverse actually). fmri_glm = FirstLevelModel(t_r=7, noise_model='ar1', @@ -158,8 +156,8 @@ # ----------------------------------------- # # To access the estimated coefficients (Betas of the GLM model), we -# created constrast with a single '1' in each of the columns: The role of the contrast is to select some columns of the model --and potentially weight them-- to study the associated statistics. So in a nutshell, a contrast is a linear combination of the estimated effects -# Here we can define canonical contrasts that just consider the two condition in isolation, let's call them "conditions", then a contrast that makes the difference between these conditions. +# created constrast with a single '1' in each of the columns: The role of the contrast is to select some columns of the model --and potentially weight them-- to study the associated statistics. So in a nutshell, a contrast is a weigted combination of the estimated effects. +# Here we can define canonical contrasts that just consider the two condition in isolation ---let's call them "conditions"--- then a contrast that makes the difference between these conditions. from numpy import array conditions = { @@ -169,25 +167,25 @@ ############################################################################### # We can then compare the two conditions 'active' and 'rest' by -# generating the relevant contrast: +# defining the corresponding contrast: active_minus_rest = conditions['active'] - conditions['rest'] ############################################################################### -# this is the estimated effect. It is in BOLD signal unit, but has no statistical guarantees, because it does not take into account the associated variance +# below, we compute the estimated effect. It is in BOLD signal unit, but has no statistical guarantees, because it does not take into account the associated variance. eff_map = fmri_glm.compute_contrast(active_minus_rest, output_type='effect_size') ############################################################################### -# In order to get statistical significance, we form a t-statistic, and directly convert is into z-scale. +# In order to get statistical significance, we form a t-statistic, and directly convert is into z-scale. The z-scale means that the values are scaled to match a standard Gaussian distribution (mean=0, variance=1), across voxels, if there are now effects in reality. z_map = fmri_glm.compute_contrast(active_minus_rest, output_type='z_score') ############################################################################### # Plot thresholded z scores map -# we display it on top of the average functional image of the seris (could be the anatomical image of the subject). +# we display it on top of the average functional image of the series (could be the anatomical image of the subject). # we use arbitrarily a threshold of 3.0 in z-scale. We'll see later how to use corrected thresholds. # we show to display 3 axial views: display_mode='z', cut_coords=3 @@ -196,9 +194,54 @@ title='Active minus Rest (Z>3)') plt.show() +############################################################################### +# Statistical signifiance testing +# One should worry about the statistical validity of the procedure: here we used an arbitrary threshold of 3.0 but the threshold should provide some guarantees on the risk of false detections (aka type-1 errors in statistics). One first suggestion is to control the false positive rate (fpr) at a certain level, e.g. 0.001: + +from nistats.thresholding import map_threshold +_, threshold = map_threshold(z_map, threshold=.001, height_control='fpr') +print('Uncorrected p<0.001 threshold: %.3f' % threshold) +plot_stat_map(z_map, bg_img=mean_img, threshold=threshold, + display_mode='z', cut_coords=3, black_bg=True, + title='Active minus Rest (p<0.001)') +plt.show() + +############################################################################### +# The problem is that with this you expect a fraction of 0.001 * n_voxels to show up while they're not active. A more conservative solution is to control the family wise errro rate, i.e. the probability of making ony one false detection, say at 5%. For that we use the so-called Bonferroni correction + +_, threshold = map_threshold(z_map, threshold=.05, height_control='bonferroni') +print('Bonferroni-corrected, p<0.05 threshold: %.3f' % threshold) +plot_stat_map(z_map, bg_img=mean_img, threshold=threshold, + display_mode='z', cut_coords=3, black_bg=True, + title='Active minus Rest (p<0.05, corrected)') +plt.show() + +############################################################################### +# This is quite conservative indeed ! +# A popular alternative is to control the false discovery rate, i.e. the expected proportion of false discoveries among detections. This is called the false disovery rate + +_, threshold = map_threshold(z_map, threshold=.05, height_control='fdr') +print('False Discovery rate = 0.05 threshold: %.3f' % threshold) +plot_stat_map(z_map, bg_img=mean_img, threshold=threshold, + display_mode='z', cut_coords=3, black_bg=True, + title='Active minus Rest (fdr=0.05)') +plt.show() + +############################################################################### +# Finally people like to discard isolated voxels (aka "small clusters") from these images. It is possible to generate a thresholded map with small clusters removed by providing a cluster_threshold argument. here clusters smaller than 10 voxels will be discarded. + +clean_map, threshold = map_threshold( + z_map, threshold=.05, height_control='fdr', cluster_threshold=10) +plot_stat_map(clean_map, bg_img=mean_img, threshold=threshold, + display_mode='z', cut_coords=3, black_bg=True, + title='Active minus Rest (fdr=0.05)') +plt.show() + + + ############################################################################### # We can save the effect and zscore maps to the disk -# first create a directory where you want tow rite the images +# first create a directory where you want to write the images import os outdir = 'results' From c058d0aeeade118d7335765deb1c888ad734f004 Mon Sep 17 00:00:00 2001 From: bthirion Date: Wed, 12 Sep 2018 23:15:47 +0200 Subject: [PATCH 057/210] Additional improvements --- .../plot_single_subject_single_run.py | 33 ++++++++++++++----- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/examples/01_tutorials/plot_single_subject_single_run.py b/examples/01_tutorials/plot_single_subject_single_run.py index 822c087f..b9375557 100644 --- a/examples/01_tutorials/plot_single_subject_single_run.py +++ b/examples/01_tutorials/plot_single_subject_single_run.py @@ -108,12 +108,14 @@ from nistats.first_level_model import FirstLevelModel ############################################################################### -# t_r=7(s) is the time of repetition of acquisitions -# - noise_model='ar1' specifies the noise covariance model: a lag-1 dependence -# - standardize=False means that we do not want to rescale the time series to mean 0, variance 1 -# - hrf_model='spm' means that we rely on the SPM "canonical hrf" model (without time or dispersion derivatives) -# - drift_model='cosine' means that we model the signal drifts as slow oscillating time functions -# - periodècut=160(s) defines the cutoff frequency (its inverse actually). +# Parameters of the first-level model +# +# * t_r=7(s) is the time of repetition of acquisitions +# * noise_model='ar1' specifies the noise covariance model: a lag-1 dependence +# * standardize=False means that we do not want to rescale the time series to mean 0, variance 1 +# * hrf_model='spm' means that we rely on the SPM "canonical hrf" model (without time or dispersion derivatives) +# * drift_model='cosine' means that we model the signal drifts as slow oscillating time functions +# * period_cut=160(s) defines the cutoff frequency (its inverse actually). fmri_glm = FirstLevelModel(t_r=7, noise_model='ar1', @@ -234,7 +236,7 @@ z_map, threshold=.05, height_control='fdr', cluster_threshold=10) plot_stat_map(clean_map, bg_img=mean_img, threshold=threshold, display_mode='z', cut_coords=3, black_bg=True, - title='Active minus Rest (fdr=0.05)') + title='Active minus Rest (fdr=0.05), clusters > 10 voxels') plt.show() @@ -252,5 +254,18 @@ # Then save the images in this directory from os.path import join -z_map.to_filename(join('results', 'active_vs_rest_z_map.nii.gz')) -eff_map.to_filename(join('results', 'active_vs_rest_eff_map.nii.gz')) +z_map.to_filename(join(outdir, 'active_vs_rest_z_map.nii.gz')) +eff_map.to_filename(join(outdir, 'active_vs_rest_eff_map.nii.gz')) + +############################################################################### +# Report the found positions in a table + +from nistats.reporting import get_clusters_table +table = get_clusters_table(z_map, stat_threshold=threshold, + cluster_threshold=20) +print(table) + +############################################################################### +# the table can be saved for future use + +table.to_csv(join(outdir, 'table.csv')) From 204416733d79dcb7c4aae3cf7174022797c50016 Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Thu, 13 Sep 2018 11:15:40 +0200 Subject: [PATCH 058/210] New PR due to previous PR (#218) being accidentally merged --- examples/README.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/README.txt b/examples/README.txt index 54853efa..e0c03516 100644 --- a/examples/README.txt +++ b/examples/README.txt @@ -12,3 +12,4 @@ Nistats usage examples .. contents:: **Contents** :local: :depth: 1 + From 278295c81be5e4ff077a778242153023b96f39bb Mon Sep 17 00:00:00 2001 From: bthirion Date: Fri, 14 Sep 2018 00:04:50 +0200 Subject: [PATCH 059/210] Fixed typos --- doc/introduction.rst | 25 ++++++++----------- .../plot_single_subject_single_run.py | 2 +- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/doc/introduction.rst b/doc/introduction.rst index d1fbcc45..802f95d2 100644 --- a/doc/introduction.rst +++ b/doc/introduction.rst @@ -12,13 +12,13 @@ What is nistats? .. topic:: **What is nistats?** - Nistats is a python module to perform voxel-wise analyses of functional magnetic resonance images (fMRI) using linear models. It provides functions to create design matrices, at the subject and group levels, to estimate them from images series and to compute statistical maps (contrasts). It allows to perform the same statistical analyses as `a SPM`_ or `a FSL`_ (but it does not provide tools for preprocessing stages (realignement, spatial normalization, etc.); for this, see `a nipype`_. + Nistats is a Python module to perform voxel-wise analyses of functional magnetic resonance images (fMRI) using linear models. It provides functions to create design matrices, at the subject and group levels, to estimate them from images series and to compute statistical maps (contrasts). It allows to perform the same statistical analyses as `a SPM`_ or `a FSL`_ (but it does not provide tools for preprocessing stages (realignment, spatial normalization, etc.); for this, see `a nipype`_. .. _a SPM: https://www.fil.ion.ucl.ac.uk/spm/ .. _a FSL: https://www.fmrib.ox.ac.uk/fsl -.. _a nipype: https://nipype.readthedocs.io/en/latest/> +.. _a nipype: https://nipype.readthedocs.io/en/latest/ @@ -27,15 +27,13 @@ A primer on BOLD-fMRI data analysis Functional magnetic resonance imaging (fMRI) is based on the fact that when local neural activity increases, increases in metabolism and blood flow lead to fluctuations of the relative concentrations of oxyhemoglobin (the red cells in the blood that carry oxygen) and deoxyhemoglobin (the same red cells after they have delivered the oxygen). Because oxy- and deoxy-hemoglobin have different magnetic properties (one is diamagnetic while the other is paramagnetic), they affect the local magnetic field in different ways. The signal picked up by the MRI scanner is sensitive to these modifications of the local magnetic field. To record cerebral activity, during functional sessions, the scanner is tuned to detect this "Blood Oxygen Level Dependent" (BOLD) signal. -Brain activity is measured in sessions that span several minutes, while the participant performs some a cognitive task and the scanner acquires brain images, typically every 2 or 3 seconds (the time between two successive image acquisition is called the Repetition time, or RT). +Brain activity is measured in sessions that span several minutes, while the participant performs some a cognitive task and the scanner acquires brain images, typically every 2 or 3 seconds (the time between two successive image acquisition is called the Repetition time, or TR). -A cerebral MR image provides a 3D image of the brain that can be decomposed into `a voxels`_ (the equivalent of pixels, but in 3 dimensions). The series of images acquired during a functional session provides, in each voxel, a time series of positive real number representing the MRI signal, sampled at the RT. +A cerebral MR image provides a 3D image of the brain that can be decomposed into `a voxels`_ (the equivalent of pixels, but in 3 dimensions). The series of images acquired during a functional session provides, in each voxel, a time series of positive real number representing the MRI signal, sampled at the TR. .. _a voxels: https://en.wikipedia.org/wiki/Voxel -.. figure:: images/stimulation-time-diagram.png - -.. note:: A typical step in the preprocessing of MR images, involves spatially morphing these images onto a standard template (e.g. the MNI152 template from the Montreal Neurological Institute). One this is done, the coordinates of a voxel are in the same space as the template and can be used to estimate its brain location using brain atlases. As already mentioned, the nistats package is not meant to perform spiatal preprocessing, but only statistical analyses on the voxel times series, regardless of the coordinate system. +.. note:: A typical step in the preprocessing of MR images, involves spatially morphing these images onto a standard template (e.g. the MNI152 template from the Montreal Neurological Institute). One this is done, the coordinates of a voxel are in the same space as the template and can be used to estimate its brain location using brain atlases. As already mentioned, the nistats package is not meant to perform spatial preprocessing, but only statistical analyses on the voxel times series, regardless of the coordinate system. One way to analyze times series consists in comparing them to a *model* built from our knowledge of the events that occurred during the functional session. Events can correspond to actions of the participant (e.g. button presses), presentations of sensory stimui (e.g. sound, images), or hypothesized internal processes (e.g. memorization of a stimulus), ... @@ -43,14 +41,13 @@ One way to analyze times series consists in comparing them to a *model* built fr .. figure:: images/stimulation-time-diagram.png -One expects that a brain region involved in the processing of a certain type of event (e.g. the auditory cortex for sounds), would show a time course of activation that correlates with the time-diagram of these events. If the fMRI signal directly showed neural activity and did not contain any noise, we could just look at it in various voxel and d -etect those that conform to the time-diagrams. +One expects that a brain region involved in the processing of a certain type of event (e.g. the auditory cortex for sounds), would show a time course of activation that correlates with the time-diagram of these events. If the fMRI signal directly showed neural activity and did not contain any noise, we could just look at it in various voxel and detect those that conform to the time-diagrams. -Yet, we know, from previous measurements, that the BOLD signal does not follow the exact time course of stimulus processing and the underlying neural activity. The BOLD response reflects changes in blood flow and concentrations in oxy-deoxy hemoglobin, all together forming an `a haemodynamic response`_ which is sluggish and long-lasting, as can be seen on the following figurte showing the response to an impulsive event (for example, an auditory click played to the participants). +Yet, we know, from previous measurements, that the BOLD signal does not follow the exact time course of stimulus processing and the underlying neural activity. The BOLD response reflects changes in blood flow and concentrations in oxy-deoxy hemoglobin, all together forming an `a haemodynamic response`_ which is sluggish and long-lasting, as can be seen on the following figure showing the response to an impulsive event (for example, an auditory click played to the participants). .. figure:: images/spm_iHRF.png -From the knowledge of the impulse haemodynamic response, we can build a predicted time course from the time-diagram of a type of event (The operation is known a a convolution. Remark: it assumes linearity of the BOLD response, an assumption that may be wrong in some scenarios). It is this predicted time course, also known as a *predictor*, that is compared to the actual fMRI signal. If the correlation between the predictor and the signal is higher than expected by chance, the voxel is said to exhibit a significant response to the event type. +From the knowledge of the impulse haemodynamic response, we can build a predicted time course from the time-diagram of a type of event (The operation is known a convolution. Remark: it assumes linearity of the BOLD response, an assumption that may be wrong in some scenarios). It is this predicted time course, also known as a *predictor*, that is compared to the actual fMRI signal. If the correlation between the predictor and the signal is higher than expected by chance, the voxel is said to exhibit a significant response to the event type. .. _a haemodynamic response: https://en.wikipedia.org/wiki/Haemodynamic_response @@ -58,15 +55,15 @@ From the knowledge of the impulse haemodynamic response, we can build a predicte .. figure:: images/time-course-and-model-fit-in-a-voxel.png -Correlations are computed separately at each voxel and a correlation map can be produced displaying the values of correlations (real numbers between -1 and +1) at each voxel. Generally, however, the maps presented in the papers report the significance of the correlations at each voxel, in forms of T, Z or p values for the null hypothesis test of no correlation. For example, the following figure didsplays a Z-map showing voxels responding to auditory events. Large (positive or negative) Z values are unlikely to be due to chance alone. The map is trasholded so that only voxels with a p-value less than 1/1000 are coloured. +Correlations are computed separately at each voxel and a correlation map can be produced displaying the values of correlations (real numbers between -1 and +1) at each voxel. Generally, however, the maps presented in the papers report the significance of the correlations at each voxel, in forms of T, Z or p values for the null hypothesis test of no correlation. For example, the following figure displays a Z-map showing voxels responding to auditory events. Large (positive or negative) Z values are unlikely to be due to chance alone. The map is tresholded so that only voxels with a p-value less than 1/1000 are coloured. .. note:: - Because, in this approach, hypothesis tests are conducted in parallel at many voxels, the likelyhood of making false alarms is important. This is knon as the probmlem of multiple comparisons. It is beyond the scope of this short notice to eplain ways in which this problem can be approached. Let us just mention that this issue can be addressed in nistats by using randnom permutations tests. + Because, in this approach, hypothesis tests are conducted in parallel at many voxels, the likelihood of making false alarms is important. This is known as the problem of multiple comparisons. It is beyond the scope of this short notice to explain ways in which this problem can be approached. Let us just mention that this issue can be addressed in nistats by using random permutations tests. .. figure:: images/example-spmZ_map.png -In most fMRI experiments, several predictors are needed to fullly describe the events occuring during the session -- for example, the experimenter may want to distinguish brain activities linked to the perception of auditory stimuli or to button presses. To find the effect specific to each predictor, a multiple `a linear regression`_ approach is typically used: all predictors are entered as columns in a *design-matrix* and the software finds the linear combination of these columns that best fits the signal. The weights assigned to each predictor by this linear cobination are estimates of the contribution of this predictor to the response in the voxel. One can plot this effect size maps or, maps showing their statistical significance (how unlikely theey are under the null hypothesis of no effect). +In most fMRI experiments, several predictors are needed to fullly describe the events occuring during the session -- for example, the experimenter may want to distinguish brain activities linked to the perception of auditory stimuli or to button presses. To find the effect specific to each predictor, a multiple `linear regression`_ approach is typically used: all predictors are entered as columns in a *design-matrix* and the software finds the linear combination of these columns that best fits the signal. The weights assigned to each predictor by this linear combination are estimates of the contribution of this predictor to the response in the voxel. One can plot this effect size maps or, maps showing their statistical significance (how unlikely they are under the null hypothesis of no effect). .. _a linear regression: https://en.wikipedia.org/wiki/Linear_regression diff --git a/examples/01_tutorials/plot_single_subject_single_run.py b/examples/01_tutorials/plot_single_subject_single_run.py index b9375557..00141f6d 100644 --- a/examples/01_tutorials/plot_single_subject_single_run.py +++ b/examples/01_tutorials/plot_single_subject_single_run.py @@ -125,7 +125,7 @@ period_cut=160) ############################################################################### -# Now that we have specified the mdoel, we can run it on the fMRI image +# Now that we have specified the model, we can run it on the fMRI image fmri_glm = fmri_glm.fit(fmri_img, events) ############################################################################### From 0f456d0eff5fee76d748e5db0d5494f74f40443f Mon Sep 17 00:00:00 2001 From: bthirion Date: Tue, 14 Aug 2018 00:09:00 +0200 Subject: [PATCH 060/210] Added Oasis example --- examples/03_second_level_models/plot_oasis.py | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 examples/03_second_level_models/plot_oasis.py diff --git a/examples/03_second_level_models/plot_oasis.py b/examples/03_second_level_models/plot_oasis.py new file mode 100644 index 00000000..98fad897 --- /dev/null +++ b/examples/03_second_level_models/plot_oasis.py @@ -0,0 +1,115 @@ +"""Voxel-Based Morphometry on Oasis dataset +======================================== + +This example uses Voxel-Based Morphometry (VBM) to study the relationship +between aging, sex and gray matter density. + +The data come from the `OASIS `_ project. +If you use it, you need to agree with the data usage agreement available +on the website. + +It has been run through a standard VBM pipeline (using SPM8 and +NewSegment) to create VBM maps, which we study here. + +VBM analysis of aging +--------------------- + +We run a standard GLM analysis to study the association between age +and gray matter density from the VBM data. We use only 100 subjects +from the OASIS dataset to limit the memory usage. + +Note that more power would be obtained from using a larger sample of subjects. +____ + +""" +# Authors: Bertrand Thirion, , July 2018 +# Elvis Dhomatob, , Apr. 2014 +# Virgile Fritsch, , Apr 2014 +# Gael Varoquaux, Apr 2014 +import numpy as np +import matplotlib.pyplot as plt +from nilearn import datasets +from nilearn.input_data import NiftiMasker + +n_subjects = 100 # more subjects requires more memory + +############################################################################ +# Load Oasis dataset +# ------------------- +oasis_dataset = datasets.fetch_oasis_vbm(n_subjects=n_subjects) +gray_matter_map_filenames = oasis_dataset.gray_matter_maps +age = oasis_dataset.ext_vars['age'].astype(float) + +# sex is encoded as 'M' or 'F'. make it a binary variable +sex = oasis_dataset.ext_vars['mf'] == b'F' + +# print basic information on the dataset +print('First gray-matter anatomy image (3D) is located at: %s' % + oasis_dataset.gray_matter_maps[0]) # 3D data +print('First white-matter anatomy image (3D) is located at: %s' % + oasis_dataset.white_matter_maps[0]) # 3D data + +## get a mask image +gm_mask = datasets.fetch_icbm152_brain_gm_mask() + +## Since this mask has a different resolution +from nilearn.image import resample_to_img +mask_img = resample_to_img( + gm_mask, gray_matter_map_filenames[0], interpolation='nearest') + +############################################################################# +# Analyse data +# ---------------- + +from nistats.second_level_model import SecondLevelModel +import pandas as pd + +design_matrix = pd.DataFrame(np.vstack((age, sex, + np.ones(n_subjects))).T, + columns=['age', 'sex', 'intercept']) +# plot the design matrix +from nistats.reporting import plot_design_matrix +ax = plot_design_matrix(design_matrix) +ax.set_title('Second level design matrix', fontsize=12) +ax.set_ylabel('maps') +plt.tight_layout() + +# specify and fit the model +second_level_model = SecondLevelModel(smoothing_fwhm=2.0, mask=mask_img) +second_level_model.fit(gray_matter_map_filenames, + design_matrix=design_matrix) + +########################################################################## +# To estimate the contrast is very simple. We can just provide the column +# name of the design matrix. +z_map = second_level_model.compute_contrast(second_level_contrast=[1, 0, 0], + output_type='z_score') + +########################################################################### +# We threshold the second level contrast at uncorrected p < 0.001 and plot +from nilearn import plotting +from nistats.thresholding import map_threshold +_, threshold = map_threshold( + z_map, threshold=.05, height_control='fdr') + +display = plotting.plot_stat_map( + z_map, threshold=threshold, colorbar=True, display_mode='z', + cut_coords=3, + title='age effect on grey matter density (FDR < .05)') + +########################################################################### +# Can also study the effect of sex + +z_map = second_level_model.compute_contrast(second_level_contrast='sex', + output_type='z_score') +_, threshold = map_threshold( + z_map, threshold=.05, height_control='fdr') +plotting.plot_stat_map( + z_map, threshold=threshold, colorbar=True, + title='sex effect on grey matter density (FDR < .05)') + +plotting.show() +# Note that there is no significant effect of sex on grey matter density + + + From 2b5da5d28961d84f9c8d2e702dc97fc0d56e8424 Mon Sep 17 00:00:00 2001 From: bthirion Date: Wed, 15 Aug 2018 23:40:33 +0200 Subject: [PATCH 061/210] First instance of surface-based example working --- .../plot_localizer_surface_analysis.py | 110 ++++++++++++++++++ nistats/model.py | 2 +- 2 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 examples/02_first_level_models/plot_localizer_surface_analysis.py diff --git a/examples/02_first_level_models/plot_localizer_surface_analysis.py b/examples/02_first_level_models/plot_localizer_surface_analysis.py new file mode 100644 index 00000000..46df0dd5 --- /dev/null +++ b/examples/02_first_level_models/plot_localizer_surface_analysis.py @@ -0,0 +1,110 @@ +""" +First level analysis of localizer dataset +========================================= + +Full step-by-step example of fitting a GLM to experimental data and visualizing +the results. + +More specifically: + +1. A sequence of fMRI volumes are loaded +2. A design matrix describing all the effects related to the data is computed +3. a mask of the useful brain volume is computed +4. A GLM is applied to the dataset (effect/covariance, + then contrast estimation) + +""" +from os import mkdir, path + +import numpy as np +import pandas as pd +import nilearn +import nistats + +from nistats.first_level_model import FirstLevelModel + + +######################################################################### +# Prepare data and analysis parameters +# ------------------------------------- +# Prepare timing +t_r = 2.4 +slice_time_ref = 0.5 + +# Prepare data +from nistats.datasets import fetch_localizer_first_level +data = fetch_localizer_first_level() +paradigm_file = data.paradigm +paradigm = pd.read_csv(paradigm_file, sep=' ', header=None, index_col=None) +paradigm.columns = ['session', 'trial_type', 'onset'] +fmri_img = data.epi_img + +######################################################################### +# Project the fMRI image to the surface +# ------------------------------------- + +fsaverage = nilearn.datasets.fetch_surf_fsaverage() +from nilearn import surface +texture = surface.vol_to_surf(fmri_img, fsaverage.pial_right) + +######################################################################### +# Perform first level analysis +# ---------------------------- +# Create design matrix +from nistats.design_matrix import make_design_matrix +frame_times = t_r * (np.arange(texture.shape[1]) + .5) +dmtx = make_design_matrix( + frame_times, paradigm=paradigm, hrf_model='glover + derivative') + +# Setup and fit GLM +from nistats.first_level_model import run_glm +labels, res = run_glm(texture.T, dmtx.values, bins=10) + +######################################################################### +# Estimate contrasts +# ------------------ +# Specify the contrasts +contrast_matrix = np.eye(dmtx.shape[1]) +contrasts = dict([(column, contrast_matrix[i]) + for i, column in enumerate(dmtx.columns)]) + +contrasts["audio"] = contrasts["clicDaudio"] + contrasts["clicGaudio"] +\ + contrasts["calculaudio"] + contrasts["phraseaudio"] +contrasts["video"] = contrasts["clicDvideo"] + contrasts["clicGvideo"] + \ + contrasts["calculvideo"] + contrasts["phrasevideo"] +contrasts["computation"] = contrasts["calculaudio"] + contrasts["calculvideo"] +contrasts["sentences"] = contrasts["phraseaudio"] + contrasts["phrasevideo"] + +######################################################################### +# Short list of more relevant contrasts +contrasts = { + "left-right": (contrasts["clicGaudio"] + contrasts["clicGvideo"] + - contrasts["clicDaudio"] - contrasts["clicDvideo"]), + "H-V": contrasts["damier_H"] - contrasts["damier_V"], + "audio-video": contrasts["audio"] - contrasts["video"], + "video-audio": -contrasts["audio"] + contrasts["video"], + "computation-sentences": (contrasts["computation"] - + contrasts["sentences"]), + "reading-visual": contrasts["phrasevideo"] - contrasts["damier_H"] + } + +######################################################################### +# contrast estimation +from nistats.contrasts import compute_contrast +from nilearn import plotting + +for index, (contrast_id, contrast_val) in enumerate(contrasts.items()): + print(' Contrast % i out of %i: %s' % + (index + 1, len(contrasts), contrast_id)) + # compute contrasts + contrast = compute_contrast(labels, res, contrast_val, contrast_type='t') + z_score = contrast.z_score() + + plotting.plot_surf_stat_map( + fsaverage.infl_right, z_score, hemi='right', + title=contrast_id, colorbar=True, + threshold=3., bg_map=fsaverage.sulc_right) + + + +plotting.show() diff --git a/nistats/model.py b/nistats/model.py index 007eead0..eb33ad85 100644 --- a/nistats/model.py +++ b/nistats/model.py @@ -75,7 +75,7 @@ def __init__(self, theta, Y, model, cov=None, dispersion=1., nuisance=None, self.df_model = model.df_model # put this as a parameter of LikelihoodModel self.df_resid = self.df_total - self.df_model - + @setattr_on_read def logL(self): """ From f06c221459d226a1b65f8da511d813dad36dfd57 Mon Sep 17 00:00:00 2001 From: bthirion Date: Fri, 24 Aug 2018 09:14:25 +0200 Subject: [PATCH 062/210] Example working, 2 hemispheres --- .../plot_localizer_surface_analysis.py | 38 ++++++++++++++----- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/examples/02_first_level_models/plot_localizer_surface_analysis.py b/examples/02_first_level_models/plot_localizer_surface_analysis.py index 46df0dd5..c3f3ef81 100644 --- a/examples/02_first_level_models/plot_localizer_surface_analysis.py +++ b/examples/02_first_level_models/plot_localizer_surface_analysis.py @@ -58,16 +58,19 @@ # Setup and fit GLM from nistats.first_level_model import run_glm -labels, res = run_glm(texture.T, dmtx.values, bins=10) +labels, res = run_glm(texture.T, dmtx.values) ######################################################################### # Estimate contrasts # ------------------ # Specify the contrasts contrast_matrix = np.eye(dmtx.shape[1]) + +# first create elementary contrasts contrasts = dict([(column, contrast_matrix[i]) for i, column in enumerate(dmtx.columns)]) +# create some intermediate contrasts contrasts["audio"] = contrasts["clicDaudio"] + contrasts["clicGaudio"] +\ contrasts["calculaudio"] + contrasts["phraseaudio"] contrasts["video"] = contrasts["clicDvideo"] + contrasts["clicGvideo"] + \ @@ -78,14 +81,14 @@ ######################################################################### # Short list of more relevant contrasts contrasts = { - "left-right": (contrasts["clicGaudio"] + contrasts["clicGvideo"] - - contrasts["clicDaudio"] - contrasts["clicDvideo"]), - "H-V": contrasts["damier_H"] - contrasts["damier_V"], - "audio-video": contrasts["audio"] - contrasts["video"], - "video-audio": -contrasts["audio"] + contrasts["video"], - "computation-sentences": (contrasts["computation"] - - contrasts["sentences"]), - "reading-visual": contrasts["phrasevideo"] - contrasts["damier_H"] + "left - right button press": ( + contrasts["clicGaudio"] + contrasts["clicGvideo"] + - contrasts["clicDaudio"] - contrasts["clicDvideo"]), + "horizontal - vertical checkerboard": ( + contrasts["damier_H"] - contrasts["damier_V"]), + "audio - video": contrasts["audio"] - contrasts["video"], + "computation - sentences": (contrasts["computation"] - + contrasts["sentences"]) } ######################################################################### @@ -105,6 +108,21 @@ title=contrast_id, colorbar=True, threshold=3., bg_map=fsaverage.sulc_right) +######################################################################### +# Analysing the left hemisphere +# Note that it requires little additional code +texture = surface.vol_to_surf(fmri_img, fsaverage.pial_left) +labels, res = run_glm(texture.T, dmtx.values) +for index, (contrast_id, contrast_val) in enumerate(contrasts.items()): + print(' Contrast % i out of %i: %s' % + (index + 1, len(contrasts), contrast_id)) + # compute contrasts + contrast = compute_contrast(labels, res, contrast_val, contrast_type='t') + z_score = contrast.z_score() + + plotting.plot_surf_stat_map( + fsaverage.infl_left, z_score, hemi='left', + title=contrast_id, colorbar=True, + threshold=3., bg_map=fsaverage.sulc_left) - plotting.show() From 5b639d53ce1b95660e71014b95917d6885be8a3e Mon Sep 17 00:00:00 2001 From: bthirion Date: Sun, 26 Aug 2018 22:33:28 +0200 Subject: [PATCH 063/210] bumped software requirements --- nistats/version.py | 1 + 1 file changed, 1 insertion(+) diff --git a/nistats/version.py b/nistats/version.py index 22cd58fc..d39dc5b9 100644 --- a/nistats/version.py +++ b/nistats/version.py @@ -37,6 +37,7 @@ 'install_info': _NISTATS_INSTALL_MSG}), ('sklearn', { 'pypi_name': 'scikit-learn', + 'min_version': '0.18', 'install_info': _NISTATS_INSTALL_MSG}), ('nibabel', { From ea1735ee6c122dd9ee6a2522c313e70162690ab3 Mon Sep 17 00:00:00 2001 From: bthirion Date: Sun, 26 Aug 2018 23:28:40 +0200 Subject: [PATCH 064/210] nilearn 0.4.0 --- nistats/version.py | 1 - 1 file changed, 1 deletion(-) diff --git a/nistats/version.py b/nistats/version.py index d39dc5b9..22cd58fc 100644 --- a/nistats/version.py +++ b/nistats/version.py @@ -37,7 +37,6 @@ 'install_info': _NISTATS_INSTALL_MSG}), ('sklearn', { 'pypi_name': 'scikit-learn', - 'min_version': '0.18', 'install_info': _NISTATS_INSTALL_MSG}), ('nibabel', { From d1eba7b00b60cfa19fbca8cec84297aecd89a6b7 Mon Sep 17 00:00:00 2001 From: bthirion Date: Sun, 26 Aug 2018 23:49:08 +0200 Subject: [PATCH 065/210] Struggling with fetch_fsaverage --- .../02_first_level_models/plot_localizer_surface_analysis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/02_first_level_models/plot_localizer_surface_analysis.py b/examples/02_first_level_models/plot_localizer_surface_analysis.py index c3f3ef81..67264fe5 100644 --- a/examples/02_first_level_models/plot_localizer_surface_analysis.py +++ b/examples/02_first_level_models/plot_localizer_surface_analysis.py @@ -43,7 +43,7 @@ # Project the fMRI image to the surface # ------------------------------------- -fsaverage = nilearn.datasets.fetch_surf_fsaverage() +fsaverage = nilearn.datasets.fetch_surf_fsaverage5() from nilearn import surface texture = surface.vol_to_surf(fmri_img, fsaverage.pial_right) From 18a408fee3b8eee787636e7a640bc1aada553733 Mon Sep 17 00:00:00 2001 From: bthirion Date: Tue, 28 Aug 2018 19:30:33 +0200 Subject: [PATCH 066/210] removed unused imports --- examples/02_first_level_models/plot_localizer_analysis.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/examples/02_first_level_models/plot_localizer_analysis.py b/examples/02_first_level_models/plot_localizer_analysis.py index 647bb2d0..88681f53 100644 --- a/examples/02_first_level_models/plot_localizer_analysis.py +++ b/examples/02_first_level_models/plot_localizer_analysis.py @@ -14,8 +14,6 @@ then contrast estimation) """ -from os import mkdir, path - import numpy as np import pandas as pd from nilearn import plotting From fac40b41a23ae5edefd15f2bce3302e9d595d57f Mon Sep 17 00:00:00 2001 From: bthirion Date: Tue, 28 Aug 2018 19:30:49 +0200 Subject: [PATCH 067/210] decluttering the figures --- .../plot_localizer_surface_analysis.py | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/examples/02_first_level_models/plot_localizer_surface_analysis.py b/examples/02_first_level_models/plot_localizer_surface_analysis.py index 67264fe5..f798d762 100644 --- a/examples/02_first_level_models/plot_localizer_surface_analysis.py +++ b/examples/02_first_level_models/plot_localizer_surface_analysis.py @@ -14,15 +14,9 @@ then contrast estimation) """ -from os import mkdir, path - import numpy as np import pandas as pd import nilearn -import nistats - -from nistats.first_level_model import FirstLevelModel - ######################################################################### # Prepare data and analysis parameters @@ -70,7 +64,7 @@ contrasts = dict([(column, contrast_matrix[i]) for i, column in enumerate(dmtx.columns)]) -# create some intermediate contrasts +# create some intermediate contrasts contrasts["audio"] = contrasts["clicDaudio"] + contrasts["clicGaudio"] +\ contrasts["calculaudio"] + contrasts["phraseaudio"] contrasts["video"] = contrasts["clicDvideo"] + contrasts["clicGvideo"] + \ @@ -84,8 +78,6 @@ "left - right button press": ( contrasts["clicGaudio"] + contrasts["clicGvideo"] - contrasts["clicDaudio"] - contrasts["clicDvideo"]), - "horizontal - vertical checkerboard": ( - contrasts["damier_H"] - contrasts["damier_V"]), "audio - video": contrasts["audio"] - contrasts["video"], "computation - sentences": (contrasts["computation"] - contrasts["sentences"]) @@ -93,11 +85,11 @@ ######################################################################### # contrast estimation -from nistats.contrasts import compute_contrast +from nistats.contrasts import compute_contrast from nilearn import plotting for index, (contrast_id, contrast_val) in enumerate(contrasts.items()): - print(' Contrast % i out of %i: %s' % + print(' Contrast % i out of %i: %s, right hemisphere' % (index + 1, len(contrasts), contrast_id)) # compute contrasts contrast = compute_contrast(labels, res, contrast_val, contrast_type='t') @@ -114,7 +106,7 @@ texture = surface.vol_to_surf(fmri_img, fsaverage.pial_left) labels, res = run_glm(texture.T, dmtx.values) for index, (contrast_id, contrast_val) in enumerate(contrasts.items()): - print(' Contrast % i out of %i: %s' % + print(' Contrast % i out of %i: %s, left hemisphere' % (index + 1, len(contrasts), contrast_id)) # compute contrasts contrast = compute_contrast(labels, res, contrast_val, contrast_type='t') From b05a80350ea97d08856278cb30381c6f7a0df9bb Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Wed, 19 Sep 2018 15:02:45 +0200 Subject: [PATCH 068/210] Added wrapper function to read experimental paradigm from tsv files - Added nistats.experimental_paradigm.paradigm_from_tsv() . - Moved `import pandas` from individual functions to module scope. --- nistats/experimental_paradigm.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/nistats/experimental_paradigm.py b/nistats/experimental_paradigm.py index afe2b003..c83e42d9 100644 --- a/nistats/experimental_paradigm.py +++ b/nistats/experimental_paradigm.py @@ -14,8 +14,9 @@ Author: Bertrand Thirion, 2015 """ from __future__ import with_statement -import warnings import numpy as np +import pandas +import warnings def check_paradigm(paradigm): @@ -80,5 +81,23 @@ def paradigm_from_csv(csv_file): paradigm : pandas DataFrame, Holding the paradigm information. """ - import pandas return pandas.read_csv(csv_file) + + +def paradigm_from_tsv(tsv_file): + """Utility function to directly read the paradigm from a tsv file + + This is simply meant to avoid explicitly import pandas everywhere. + + Parameters + ---------- + tsv_file : string, + Path to a tsv file. + + Returns + ------- + paradigm : pandas DataFrame, + Holding the paradigm information. + """ + return pandas.read_table(tsv_file) + From 089b26a595e90073c54fe53dda485c90195d3238 Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Thu, 20 Sep 2018 11:15:25 +0200 Subject: [PATCH 069/210] Removed unnecessary wrapper functions to read paradigm from tsv,csv files - Removed nistats.experimental_paradigm.paradigm_from_csv() . - Removed nistats.experimental_paradigm.paradigm_from_tsv() added in the previous commit. - test_paradigm.py now directly uses pandas.read_csv() --- nistats/experimental_paradigm.py | 37 -------------------------------- nistats/tests/test_paradigm.py | 4 +--- 2 files changed, 1 insertion(+), 40 deletions(-) diff --git a/nistats/experimental_paradigm.py b/nistats/experimental_paradigm.py index c83e42d9..a2c29e60 100644 --- a/nistats/experimental_paradigm.py +++ b/nistats/experimental_paradigm.py @@ -64,40 +64,3 @@ def check_paradigm(paradigm): warnings.warn("'modulation' key not found in the given paradigm.") modulation = np.array(paradigm['modulation']).astype(np.float) return trial_type, onset, duration, modulation - - -def paradigm_from_csv(csv_file): - """Utility function to directly read the paradigm from a csv file - - This is simply meant to avoid explicitly import pandas everywhere. - - Parameters - ---------- - csv_file : string, - Path to a csv file. - - Returns - ------- - paradigm : pandas DataFrame, - Holding the paradigm information. - """ - return pandas.read_csv(csv_file) - - -def paradigm_from_tsv(tsv_file): - """Utility function to directly read the paradigm from a tsv file - - This is simply meant to avoid explicitly import pandas everywhere. - - Parameters - ---------- - tsv_file : string, - Path to a tsv file. - - Returns - ------- - paradigm : pandas DataFrame, - Holding the paradigm information. - """ - return pandas.read_table(tsv_file) - diff --git a/nistats/tests/test_paradigm.py b/nistats/tests/test_paradigm.py index 15368742..37ae4a26 100644 --- a/nistats/tests/test_paradigm.py +++ b/nistats/tests/test_paradigm.py @@ -9,8 +9,6 @@ import os import pandas as pd -from nistats.experimental_paradigm import paradigm_from_csv - from nose.tools import assert_true @@ -71,5 +69,5 @@ def test_read_paradigm(): modulated_block_paradigm(), basic_paradigm()): csvfile = write_paradigm(paradigm, tmpdir) - read_paradigm = paradigm_from_csv(csvfile) + read_paradigm = pd.read_csv(csvfile) assert_true((read_paradigm['onset'] == paradigm['onset']).all()) From 48ef5065452e455aadf19621254b3b22a3ba1df1 Mon Sep 17 00:00:00 2001 From: bthirion Date: Thu, 20 Sep 2018 22:03:10 +0200 Subject: [PATCH 070/210] Added an F test --- .../plot_single_subject_single_run.py | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/examples/01_tutorials/plot_single_subject_single_run.py b/examples/01_tutorials/plot_single_subject_single_run.py index 00141f6d..21f7713d 100644 --- a/examples/01_tutorials/plot_single_subject_single_run.py +++ b/examples/01_tutorials/plot_single_subject_single_run.py @@ -240,7 +240,6 @@ plt.show() - ############################################################################### # We can save the effect and zscore maps to the disk # first create a directory where you want to write the images @@ -269,3 +268,27 @@ # the table can be saved for future use table.to_csv(join(outdir, 'table.csv')) + +############################################################################### +# Performing an F-test +# +# "active vs rest" is a typical t test: condition versus baseline. Another popular type of test is an F test in which one seeks whether a certain combination of conditions (possibly two-, three- or higher-dimensional) explains a significant proportion of the signal. +# Here one might for instance test which voxels are well explained by combination of the active and rest condition. Atcually, the contrast specification is done exactly the same way as for t contrasts. + +import numpy as np +effects_of_interest = np.vstack((conditions['active'], conditions['rest'])) +z_map = fmri_glm.compute_contrast(effects_of_interest, + output_type='z_score') +from nistats.reporting import plot_contrast_matrix +plot_contrast_matrix(effects_of_interest, design_matrix) +plt.show() + +############################################################################### +# Note that the statistic has been converted to a z-variable, which makes it easier to represent it. + +clean_map, threshold = map_threshold( + z_map, threshold=.05, height_control='fdr', cluster_threshold=10) +plot_stat_map(clean_map, bg_img=mean_img, threshold=threshold, + display_mode='z', cut_coords=3, black_bg=True, + title='Effects of interest (fdr=0.05), clusters > 10 voxels') +plt.show() From dbf6ff47ded2d83ced7d6b87a1f2a61fa4927eac Mon Sep 17 00:00:00 2001 From: bthirion Date: Fri, 21 Sep 2018 14:44:29 +0200 Subject: [PATCH 071/210] changed default oversampling to 50 --- nistats/design_matrix.py | 18 +++++++++--------- nistats/hemodynamic_models.py | 24 ++++++++++++------------ nistats/tests/test_dmtx.py | 2 +- nistats/tests/test_hemodynamic_models.py | 18 +++++++++--------- 4 files changed, 31 insertions(+), 31 deletions(-) diff --git a/nistats/design_matrix.py b/nistats/design_matrix.py index 53137e8d..4de3dea2 100644 --- a/nistats/design_matrix.py +++ b/nistats/design_matrix.py @@ -164,7 +164,7 @@ def _make_drift(drift_model, frame_times, order=1, period_cut=128.): def _convolve_regressors(paradigm, hrf_model, frame_times, fir_delays=[0], - min_onset=-24, oversampling=None): + min_onset=-24, oversampling=50): """ Creation of a matrix that comprises the convolution of the conditions onset with a certain hrf model @@ -191,9 +191,9 @@ def _convolve_regressors(paradigm, hrf_model, frame_times, fir_delays=[0], Minimal onset relative to frame_times[0] (in seconds) events that start before frame_times[0] + min_onset are not considered. - oversampling: float or None, optional, - Oversampling factor used in temporal convolutions. Should be 1 for - whenever hrf_mode is 'fir' and 16 otherwise. + oversampling: int or None, optional, default:50, + Oversampling factor used in temporal convolutions. + Should be 1 whenever hrf_model is 'fir'. Returns ------- @@ -219,7 +219,7 @@ def _convolve_regressors(paradigm, hrf_model, frame_times, fir_delays=[0], 'impulse response hrf model') oversampling = 1 elif oversampling is None: - oversampling = 16 + oversampling = 50 trial_type, onset, duration, modulation = check_paradigm(paradigm) for condition in np.unique(trial_type): @@ -281,7 +281,7 @@ def _full_rank(X, cmax=1e15): def make_design_matrix( frame_times, paradigm=None, hrf_model='glover', drift_model='cosine', period_cut=128, drift_order=1, fir_delays=[0], - add_regs=None, add_reg_names=None, min_onset=-24, oversampling=None): + add_regs=None, add_reg_names=None, min_onset=-24, oversampling=50): """Generate a design matrix from the input parameters Parameters @@ -339,9 +339,9 @@ def make_design_matrix( Minimal onset relative to frame_times[0] (in seconds) events that start before frame_times[0] + min_onset are not considered. - oversampling: float or None, optional, - Oversampling factor used in temporal convolutions. Should be 1 for - whenever hrf_mode is 'fir' and 16 otherwise. + oversampling: int or None, optional, + Oversampling factor used in temporal convolutions. + Should be 1 whenever hrf_model is 'fir'. Returns ------- diff --git a/nistats/hemodynamic_models.py b/nistats/hemodynamic_models.py index a7da6057..b20c13ec 100644 --- a/nistats/hemodynamic_models.py +++ b/nistats/hemodynamic_models.py @@ -3,7 +3,7 @@ Here we provide for SPM, Glover hrfs and finite timpulse response (FIR) models. This module closely follows SPM implementation -Author: Bertrand Thirion, 2011--2015 +Author: Bertrand Thirion, 2011--2018 """ import warnings @@ -11,7 +11,7 @@ from scipy.stats import gamma -def _gamma_difference_hrf(tr, oversampling=16, time_length=32., onset=0., +def _gamma_difference_hrf(tr, oversampling=50, time_length=32., onset=0., delay=6, undershoot=16., dispersion=1., u_dispersion=1., ratio=0.167): """ Compute an hrf as the difference of two gamma functions @@ -22,7 +22,7 @@ def _gamma_difference_hrf(tr, oversampling=16, time_length=32., onset=0., tr : float scan repeat time, in seconds - oversampling : int, optional (default=16) + oversampling : int, optional (default=50) temporal oversampling factor time_length : float, optional (default=32) @@ -61,7 +61,7 @@ def _gamma_difference_hrf(tr, oversampling=16, time_length=32., onset=0., return hrf -def spm_hrf(tr, oversampling=16, time_length=32., onset=0.): +def spm_hrf(tr, oversampling=50, time_length=32., onset=0.): """ Implementation of the SPM hrf model Parameters @@ -86,7 +86,7 @@ def spm_hrf(tr, oversampling=16, time_length=32., onset=0.): return _gamma_difference_hrf(tr, oversampling, time_length, onset) -def glover_hrf(tr, oversampling=16, time_length=32., onset=0.): +def glover_hrf(tr, oversampling=50, time_length=32., onset=0.): """ Implementation of the Glover hrf model Parameters @@ -113,7 +113,7 @@ def glover_hrf(tr, oversampling=16, time_length=32., onset=0.): u_dispersion=.9, ratio=.35) -def spm_time_derivative(tr, oversampling=16, time_length=32., onset=0.): +def spm_time_derivative(tr, oversampling=50, time_length=32., onset=0.): """Implementation of the SPM time derivative hrf (dhrf) model Parameters @@ -141,7 +141,7 @@ def spm_time_derivative(tr, oversampling=16, time_length=32., onset=0.): return dhrf -def glover_time_derivative(tr, oversampling=16, time_length=32., onset=0.): +def glover_time_derivative(tr, oversampling=50, time_length=32., onset=0.): """Implementation of the Glover time derivative hrf (dhrf) model Parameters @@ -166,7 +166,7 @@ def glover_time_derivative(tr, oversampling=16, time_length=32., onset=0.): return dhrf -def spm_dispersion_derivative(tr, oversampling=16, time_length=32., onset=0.): +def spm_dispersion_derivative(tr, oversampling=50, time_length=32., onset=0.): """Implementation of the SPM dispersion derivative hrf model Parameters @@ -196,7 +196,7 @@ def spm_dispersion_derivative(tr, oversampling=16, time_length=32., onset=0.): return dhrf -def glover_dispersion_derivative(tr, oversampling=16, time_length=32., +def glover_dispersion_derivative(tr, oversampling=50, time_length=32., onset=0.): """Implementation of the Glover dispersion derivative hrf model @@ -230,7 +230,7 @@ def glover_dispersion_derivative(tr, oversampling=16, time_length=32., return dhrf -def _sample_condition(exp_condition, frame_times, oversampling=16, +def _sample_condition(exp_condition, frame_times, oversampling=50, min_onset=-24): """Make a possibly oversampled event regressor from condition information. @@ -374,7 +374,7 @@ def _regressor_names(con_name, hrf_model, fir_delays=None): return [con_name + "_delay_%d" % i for i in fir_delays] -def _hrf_kernel(hrf_model, tr, oversampling=16, fir_delays=None): +def _hrf_kernel(hrf_model, tr, oversampling=50, fir_delays=None): """ Given the specification of the hemodynamic model and time parameters, return the list of matching kernels @@ -432,7 +432,7 @@ def _hrf_kernel(hrf_model, tr, oversampling=16, fir_delays=None): def compute_regressor(exp_condition, hrf_model, frame_times, con_id='cond', - oversampling=16, fir_delays=None, min_onset=-24): + oversampling=50, fir_delays=None, min_onset=-24): """ This is the main function to convolve regressors with hrf model Parameters diff --git a/nistats/tests/test_dmtx.py b/nistats/tests/test_dmtx.py index 2b0b76a7..0e8defcf 100644 --- a/nistats/tests/test_dmtx.py +++ b/nistats/tests/test_dmtx.py @@ -427,7 +427,7 @@ def test_oversampling(): X1 = make_design_matrix( frame_times, paradigm, drift_model=None) X2 = make_design_matrix( - frame_times, paradigm, drift_model=None, oversampling=16) + frame_times, paradigm, drift_model=None, oversampling=50) X3 = make_design_matrix( frame_times, paradigm, drift_model=None, oversampling=10) diff --git a/nistats/tests/test_hemodynamic_models.py b/nistats/tests/test_hemodynamic_models.py index ba333b7f..d84f1067 100644 --- a/nistats/tests/test_hemodynamic_models.py +++ b/nistats/tests/test_hemodynamic_models.py @@ -17,7 +17,7 @@ def test_spm_hrf(): """ h = spm_hrf(2.0) assert_almost_equal(h.sum(), 1) - assert_equal(len(h), 256) + assert_equal(len(h), 800) def test_spm_hrf_derivative(): @@ -25,10 +25,10 @@ def test_spm_hrf_derivative(): """ h = spm_time_derivative(2.0) assert_almost_equal(h.sum(), 0) - assert_equal(len(h), 256) + assert_equal(len(h), 800) h = spm_dispersion_derivative(2.0) assert_almost_equal(h.sum(), 0) - assert_equal(len(h), 256) + assert_equal(len(h), 800) def test_glover_hrf(): @@ -36,10 +36,10 @@ def test_glover_hrf(): """ h = glover_hrf(2.0) assert_almost_equal(h.sum(), 1) - assert_equal(len(h), 256) + assert_equal(len(h), 800) h = glover_dispersion_derivative(2.0) assert_almost_equal(h.sum(), 0) - assert_equal(len(h), 256) + assert_equal(len(h), 800) @@ -48,7 +48,7 @@ def test_glover_time_derivative(): """ h = glover_time_derivative(2.0) assert_almost_equal(h.sum(), 0) - assert_equal(len(h), 256) + assert_equal(len(h), 800) def test_resample_regressor(): @@ -195,11 +195,11 @@ def test_hkernel(): h = _hrf_kernel('fir', tr, fir_delays=np.arange(4)) assert_equal(len(h), 4) for dh in h: - assert_equal(dh.sum(), 16.) + assert_equal(dh.sum(), 50.) # h = _hrf_kernel(None, tr) assert_equal(len(h), 1) - assert_almost_equal(h[0], np.hstack((1, np.zeros(15)))) + assert_almost_equal(h[0], np.hstack((1, np.zeros(49)))) def test_make_regressor_1(): @@ -221,7 +221,7 @@ def test_make_regressor_2(): frame_times = np.linspace(0, 69, 70) hrf_model = 'spm' reg, reg_names = compute_regressor(condition, hrf_model, frame_times) - assert_almost_equal(reg.sum() * 16, 3, 1) + assert_almost_equal(reg.sum() * 50, 3, 1) assert_equal(reg_names[0], 'cond') From 43ff1c6d695e90ecf83debd27c55ace3394ef6d5 Mon Sep 17 00:00:00 2001 From: bthirion Date: Fri, 21 Sep 2018 15:55:18 +0200 Subject: [PATCH 072/210] Choosing plotting coordinates --- examples/03_second_level_models/plot_oasis.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/examples/03_second_level_models/plot_oasis.py b/examples/03_second_level_models/plot_oasis.py index 98fad897..1793d3a0 100644 --- a/examples/03_second_level_models/plot_oasis.py +++ b/examples/03_second_level_models/plot_oasis.py @@ -64,8 +64,8 @@ from nistats.second_level_model import SecondLevelModel import pandas as pd -design_matrix = pd.DataFrame(np.vstack((age, sex, - np.ones(n_subjects))).T, +intercept = np.ones(n_subjects) +design_matrix = pd.DataFrame(np.vstack((age, sex, intercept)).T, columns=['age', 'sex', 'intercept']) # plot the design matrix from nistats.reporting import plot_design_matrix @@ -74,7 +74,9 @@ ax.set_ylabel('maps') plt.tight_layout() +########################################################################## # specify and fit the model + second_level_model = SecondLevelModel(smoothing_fwhm=2.0, mask=mask_img) second_level_model.fit(gray_matter_map_filenames, design_matrix=design_matrix) @@ -94,7 +96,7 @@ display = plotting.plot_stat_map( z_map, threshold=threshold, colorbar=True, display_mode='z', - cut_coords=3, + cut_coords=[-4, 26], title='age effect on grey matter density (FDR < .05)') ########################################################################### @@ -109,7 +111,5 @@ title='sex effect on grey matter density (FDR < .05)') plotting.show() -# Note that there is no significant effect of sex on grey matter density - - - +########################################################################### +# Note that there is no significant effect of sex on grey matter density. From a1d2bbbd87f666535b9221f2374de3303bb7eb50 Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Fri, 21 Sep 2018 16:36:54 +0200 Subject: [PATCH 073/210] Removed unnecessary wrapper functions to read paradigm from tsv,csv files - Removed nistats.experimental_paradigm.paradigm_from_csv() . - Removed nistats.experimental_paradigm.paradigm_from_tsv() added in the previous commit. - test_paradigm.py now directly uses pandas.read_csv() --- nistats/first_level_model.py | 8 ++-- nistats/utils.py | 74 ++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 3 deletions(-) diff --git a/nistats/first_level_model.py b/nistats/first_level_model.py index 5f7a715d..4d1945a7 100644 --- a/nistats/first_level_model.py +++ b/nistats/first_level_model.py @@ -12,6 +12,7 @@ """ from warnings import warn +import csv import time import sys import os @@ -33,8 +34,9 @@ from .regression import OLSModel, ARModel, SimpleRegressionResults from .design_matrix import make_design_matrix from .contrasts import _fixed_effect_contrast -from .utils import (_basestring, _check_run_tables, get_bids_files, - parse_bids_filename) +from .utils import (_basestring, _check_run_tables, + _verify_file_value_separators_are_tabs_commas, + get_bids_files, parse_bids_filename) def mean_scaling(Y, axis=0): @@ -355,8 +357,8 @@ def fit(self, run_imgs, events=None, confounds=None, # Check that number of events and confound files match number of runs # Also check that events and confound files can be loaded as DataFrame if events is not None: + _verify_file_value_separators_are_tabs_commas(filepaths=events) events = _check_run_tables(run_imgs, events, 'events') - if confounds is not None: confounds = _check_run_tables(run_imgs, confounds, 'confounds') diff --git a/nistats/utils.py b/nistats/utils.py index 6668d2a3..34c2fa7e 100644 --- a/nistats/utils.py +++ b/nistats/utils.py @@ -2,6 +2,8 @@ Authors: Bertrand Thirion, Matthew Brett, 2015 """ +import csv +from pathlib import Path import sys import scipy.linalg as spl import numpy as np @@ -50,6 +52,78 @@ def _check_run_tables(run_imgs, tables_, tables_name): return tables_ +def _verify_delimiters_used(filepaths, delimiters=None): + """ Accepts a list of filepaths and verifies the delimiter used. + Raises an error if they do not use one of the specified delimiters + or if the file cannot be read. + If no delimiters specified, + checks for presence of any standard delimiter. + + Parameters + ---------- + filepaths: list[str], + A list/tuple of filepaths of files to be verified. + + delimiters: list[str], None (default) + A list/tuple of allowed characters for separating values. + If None, will check for presence of any standard delimiter. + + Returns + ------- + None + + Raises + ------ + ValueError: + If the file does not use one of the indicated delimiters. + TypeError: + If the filepath points to a non-text file (usually a csv or tsv). + FileNotFoundError: + If the filepath is incorrect or the file does not exist. + """ + filepaths = [filepaths] if isinstance(filepaths, str) else filepaths + for filepath_ in filepaths: + try: + sample = Path(filepath_).read_text() + except (IsADirectoryError, FileNotFoundError): + raise FileNotFoundError('Not a valid filepath, or file does not exist.', filepath_) + except TypeError: + raise TypeError('Not a readable text file.', filepath_) + + try: + csv.Sniffer().sniff(sample=sample, delimiters=delimiters,) + except csv.Error as csv_err: + raise csv.Error(filepath_) + +def _verify_file_value_separators_are_tabs_commas(filepaths): + """ + Accepts list/tuple of paths for events files (tsv or csv) and + verifies they use only tabs or commas to spearate their values. + + Parameters + ---------- + filepaths: List[str], Tuple[str] + + Returns + ------- + None + + Raises + ------ + ValueError: + If tabs or commas are not the detected separators. + """ + valid_delimiters = [',', '\t', ] + try: + _verify_delimiters_used(filepaths=filepaths, delimiters=valid_delimiters) + except (FileNotFoundError, TypeError): + pass + except csv.Error as err: + raise ValueError('The provided text file does not seem to use ' + 'either tabs or commas for separating values ' + , err.args) from err + + def z_score(pvalue): """ Return the z-score corresponding to a given p-value. """ From 96e32f3f39eeff3ff87999cba546ff6c55b955bd Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Fri, 21 Sep 2018 16:36:54 +0200 Subject: [PATCH 074/210] Added function to raise error if events file doesn't use tabs or commas - This commit was pushed earlier without an updated commit message, this commit intends to fix that --- nistats/first_level_model.py | 8 ++-- nistats/utils.py | 74 ++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 3 deletions(-) diff --git a/nistats/first_level_model.py b/nistats/first_level_model.py index 5f7a715d..4d1945a7 100644 --- a/nistats/first_level_model.py +++ b/nistats/first_level_model.py @@ -12,6 +12,7 @@ """ from warnings import warn +import csv import time import sys import os @@ -33,8 +34,9 @@ from .regression import OLSModel, ARModel, SimpleRegressionResults from .design_matrix import make_design_matrix from .contrasts import _fixed_effect_contrast -from .utils import (_basestring, _check_run_tables, get_bids_files, - parse_bids_filename) +from .utils import (_basestring, _check_run_tables, + _verify_file_value_separators_are_tabs_commas, + get_bids_files, parse_bids_filename) def mean_scaling(Y, axis=0): @@ -355,8 +357,8 @@ def fit(self, run_imgs, events=None, confounds=None, # Check that number of events and confound files match number of runs # Also check that events and confound files can be loaded as DataFrame if events is not None: + _verify_file_value_separators_are_tabs_commas(filepaths=events) events = _check_run_tables(run_imgs, events, 'events') - if confounds is not None: confounds = _check_run_tables(run_imgs, confounds, 'confounds') diff --git a/nistats/utils.py b/nistats/utils.py index 6668d2a3..34c2fa7e 100644 --- a/nistats/utils.py +++ b/nistats/utils.py @@ -2,6 +2,8 @@ Authors: Bertrand Thirion, Matthew Brett, 2015 """ +import csv +from pathlib import Path import sys import scipy.linalg as spl import numpy as np @@ -50,6 +52,78 @@ def _check_run_tables(run_imgs, tables_, tables_name): return tables_ +def _verify_delimiters_used(filepaths, delimiters=None): + """ Accepts a list of filepaths and verifies the delimiter used. + Raises an error if they do not use one of the specified delimiters + or if the file cannot be read. + If no delimiters specified, + checks for presence of any standard delimiter. + + Parameters + ---------- + filepaths: list[str], + A list/tuple of filepaths of files to be verified. + + delimiters: list[str], None (default) + A list/tuple of allowed characters for separating values. + If None, will check for presence of any standard delimiter. + + Returns + ------- + None + + Raises + ------ + ValueError: + If the file does not use one of the indicated delimiters. + TypeError: + If the filepath points to a non-text file (usually a csv or tsv). + FileNotFoundError: + If the filepath is incorrect or the file does not exist. + """ + filepaths = [filepaths] if isinstance(filepaths, str) else filepaths + for filepath_ in filepaths: + try: + sample = Path(filepath_).read_text() + except (IsADirectoryError, FileNotFoundError): + raise FileNotFoundError('Not a valid filepath, or file does not exist.', filepath_) + except TypeError: + raise TypeError('Not a readable text file.', filepath_) + + try: + csv.Sniffer().sniff(sample=sample, delimiters=delimiters,) + except csv.Error as csv_err: + raise csv.Error(filepath_) + +def _verify_file_value_separators_are_tabs_commas(filepaths): + """ + Accepts list/tuple of paths for events files (tsv or csv) and + verifies they use only tabs or commas to spearate their values. + + Parameters + ---------- + filepaths: List[str], Tuple[str] + + Returns + ------- + None + + Raises + ------ + ValueError: + If tabs or commas are not the detected separators. + """ + valid_delimiters = [',', '\t', ] + try: + _verify_delimiters_used(filepaths=filepaths, delimiters=valid_delimiters) + except (FileNotFoundError, TypeError): + pass + except csv.Error as err: + raise ValueError('The provided text file does not seem to use ' + 'either tabs or commas for separating values ' + , err.args) from err + + def z_score(pvalue): """ Return the z-score corresponding to a given p-value. """ From 187584808f233af0e970319ecb94e62e7552028d Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Fri, 21 Sep 2018 16:57:08 +0200 Subject: [PATCH 075/210] Abandoned use of pathlib to maintain compatibility with older python versions --- nistats/utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nistats/utils.py b/nistats/utils.py index 34c2fa7e..32597630 100644 --- a/nistats/utils.py +++ b/nistats/utils.py @@ -3,7 +3,6 @@ Authors: Bertrand Thirion, Matthew Brett, 2015 """ import csv -from pathlib import Path import sys import scipy.linalg as spl import numpy as np @@ -84,7 +83,8 @@ def _verify_delimiters_used(filepaths, delimiters=None): filepaths = [filepaths] if isinstance(filepaths, str) else filepaths for filepath_ in filepaths: try: - sample = Path(filepath_).read_text() + with open(filepath_, 'r') as svfile: + sample = svfile.read() except (IsADirectoryError, FileNotFoundError): raise FileNotFoundError('Not a valid filepath, or file does not exist.', filepath_) except TypeError: @@ -92,7 +92,7 @@ def _verify_delimiters_used(filepaths, delimiters=None): try: csv.Sniffer().sniff(sample=sample, delimiters=delimiters,) - except csv.Error as csv_err: + except csv.Error: raise csv.Error(filepath_) def _verify_file_value_separators_are_tabs_commas(filepaths): From 01379db5159ad17ccf0759bc1b8dd525398f14d2 Mon Sep 17 00:00:00 2001 From: bthirion Date: Sat, 22 Sep 2018 23:33:20 +0200 Subject: [PATCH 076/210] doc fixes --- nistats/first_level_model.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/nistats/first_level_model.py b/nistats/first_level_model.py index 5f7a715d..96860d94 100644 --- a/nistats/first_level_model.py +++ b/nistats/first_level_model.py @@ -161,10 +161,10 @@ class FirstLevelModel(BaseEstimator, TransformerMixin, CacheMixin): expressed as a percentage of the t_r (time repetition), so it can have values between 0. and 1. - hrf_model : string, optional - This parameter specifies the hemodynamic response function (HRF) for - the design matrices. It can be 'canonical', 'canonical with derivative' - or 'fir'. + hrf_model : {'spm', 'spm + derivative', 'spm + derivative + dispersion', + 'glover', 'glover + derivative', 'glover + derivative + dispersion', + 'fir', None} + String that specifies the hemodynamic response function. Defaults to 'glover'. drift_model : string, optional This parameter specifies the desired drift model for the design @@ -316,7 +316,7 @@ def fit(self, run_imgs, events=None, confounds=None, Parameters ---------- run_imgs: Niimg-like object or list of Niimg-like objects, - See http://nilearn.github.io/building_blocks/manipulating_mr_images.html#niimg. + See http://nilearn.github.io/manipulating_images/input_output.html#inputing-data-file-names-or-image-objects Data on which the GLM will be fitted. If this is a list, the affine is considered the same for all. From c7cb7dd6d12778b13ed13786423af022a24f4f27 Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Mon, 24 Sep 2018 16:41:23 +0200 Subject: [PATCH 077/210] Starting point for py3 to py2/3 conversion; Some reformatting --- nistats/utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/nistats/utils.py b/nistats/utils.py index 32597630..87dd5d45 100644 --- a/nistats/utils.py +++ b/nistats/utils.py @@ -86,7 +86,9 @@ def _verify_delimiters_used(filepaths, delimiters=None): with open(filepath_, 'r') as svfile: sample = svfile.read() except (IsADirectoryError, FileNotFoundError): - raise FileNotFoundError('Not a valid filepath, or file does not exist.', filepath_) + raise FileNotFoundError( + 'Not a valid filepath, or file does not exist.', filepath_ + ) except TypeError: raise TypeError('Not a readable text file.', filepath_) From 3f492979601c279d7bc54b0f9259b87457084cdf Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Wed, 26 Sep 2018 00:29:27 +0200 Subject: [PATCH 078/210] Added replacement function to raise error if events files use invalid separators --- nistats/first_level_model.py | 7 +-- nistats/utils.py | 94 +++++++++--------------------------- 2 files changed, 26 insertions(+), 75 deletions(-) diff --git a/nistats/first_level_model.py b/nistats/first_level_model.py index 4d1945a7..65cdd343 100644 --- a/nistats/first_level_model.py +++ b/nistats/first_level_model.py @@ -12,7 +12,6 @@ """ from warnings import warn -import csv import time import sys import os @@ -35,7 +34,7 @@ from .design_matrix import make_design_matrix from .contrasts import _fixed_effect_contrast from .utils import (_basestring, _check_run_tables, - _verify_file_value_separators_are_tabs_commas, + _verify_events_file_uses_valid_value_separators, get_bids_files, parse_bids_filename) @@ -343,6 +342,9 @@ def fit(self, run_imgs, events=None, confounds=None, """ # Check arguments # Check imgs type + if events is not None: + _verify_events_file_uses_valid_value_separators( + events_files=events) if not isinstance(run_imgs, (list, tuple)): run_imgs = [run_imgs] if design_matrices is None: @@ -357,7 +359,6 @@ def fit(self, run_imgs, events=None, confounds=None, # Check that number of events and confound files match number of runs # Also check that events and confound files can be loaded as DataFrame if events is not None: - _verify_file_value_separators_are_tabs_commas(filepaths=events) events = _check_run_tables(run_imgs, events, 'events') if confounds is not None: confounds = _check_run_tables(run_imgs, confounds, 'confounds') diff --git a/nistats/utils.py b/nistats/utils.py index 87dd5d45..b430f5fb 100644 --- a/nistats/utils.py +++ b/nistats/utils.py @@ -42,6 +42,28 @@ def _check_and_load_tables(tables_, var_name): return tables +def _verify_events_file_uses_valid_value_separators(events_files): + valid_separators = [',', '\t'] + events_files = [events_files] if isinstance(events_files, str) else events_files + for events_file_ in events_files: + try: + with open(events_file_, 'r') as events_file_obj: + events_file_sample = events_file_obj.read(1024) + except TypeError as err: + pass + except IOError: + pass + else: + try: + csv.Sniffer().sniff(sample=events_file_sample, + delimiters=valid_separators, + ) + except csv.Error: + raise ValueError( + 'The values in the events file are not separated by tabs or commas', + events_file_) + + def _check_run_tables(run_imgs, tables_, tables_name): """Check fMRI runs and corresponding tables to raise error if necessary""" if isinstance(tables_, (_basestring, pd.DataFrame)): @@ -51,79 +73,7 @@ def _check_run_tables(run_imgs, tables_, tables_name): return tables_ -def _verify_delimiters_used(filepaths, delimiters=None): - """ Accepts a list of filepaths and verifies the delimiter used. - Raises an error if they do not use one of the specified delimiters - or if the file cannot be read. - If no delimiters specified, - checks for presence of any standard delimiter. - - Parameters - ---------- - filepaths: list[str], - A list/tuple of filepaths of files to be verified. - - delimiters: list[str], None (default) - A list/tuple of allowed characters for separating values. - If None, will check for presence of any standard delimiter. - Returns - ------- - None - - Raises - ------ - ValueError: - If the file does not use one of the indicated delimiters. - TypeError: - If the filepath points to a non-text file (usually a csv or tsv). - FileNotFoundError: - If the filepath is incorrect or the file does not exist. - """ - filepaths = [filepaths] if isinstance(filepaths, str) else filepaths - for filepath_ in filepaths: - try: - with open(filepath_, 'r') as svfile: - sample = svfile.read() - except (IsADirectoryError, FileNotFoundError): - raise FileNotFoundError( - 'Not a valid filepath, or file does not exist.', filepath_ - ) - except TypeError: - raise TypeError('Not a readable text file.', filepath_) - - try: - csv.Sniffer().sniff(sample=sample, delimiters=delimiters,) - except csv.Error: - raise csv.Error(filepath_) - -def _verify_file_value_separators_are_tabs_commas(filepaths): - """ - Accepts list/tuple of paths for events files (tsv or csv) and - verifies they use only tabs or commas to spearate their values. - - Parameters - ---------- - filepaths: List[str], Tuple[str] - - Returns - ------- - None - - Raises - ------ - ValueError: - If tabs or commas are not the detected separators. - """ - valid_delimiters = [',', '\t', ] - try: - _verify_delimiters_used(filepaths=filepaths, delimiters=valid_delimiters) - except (FileNotFoundError, TypeError): - pass - except csv.Error as err: - raise ValueError('The provided text file does not seem to use ' - 'either tabs or commas for separating values ' - , err.args) from err def z_score(pvalue): From ef1c2deb3a95e6853633aa75a3e6f52b628dac13 Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Wed, 26 Sep 2018 15:50:20 +0200 Subject: [PATCH 079/210] Renamed + Only first row is used to determine BIDS compliance of events files - Renamed _verify_events_file_uses_valid_value_separators() -> _verify_events_file_uses_tab_separators() - Added (_not_test=True) parameter; enables raising errors during unit testing. - Added UnicodeDecodeError for Python3. - Added docstring and comments. - Improved message regarding BIDS non-compliance. --- nistats/first_level_model.py | 4 +-- nistats/utils.py | 53 +++++++++++++++++++++++++++++------- 2 files changed, 45 insertions(+), 12 deletions(-) diff --git a/nistats/first_level_model.py b/nistats/first_level_model.py index 65cdd343..e6bad1cf 100644 --- a/nistats/first_level_model.py +++ b/nistats/first_level_model.py @@ -34,7 +34,7 @@ from .design_matrix import make_design_matrix from .contrasts import _fixed_effect_contrast from .utils import (_basestring, _check_run_tables, - _verify_events_file_uses_valid_value_separators, + _verify_events_file_uses_tab_separators, get_bids_files, parse_bids_filename) @@ -343,7 +343,7 @@ def fit(self, run_imgs, events=None, confounds=None, # Check arguments # Check imgs type if events is not None: - _verify_events_file_uses_valid_value_separators( + _verify_events_file_uses_tab_separators( events_files=events) if not isinstance(run_imgs, (list, tuple)): run_imgs = [run_imgs] diff --git a/nistats/utils.py b/nistats/utils.py index b430f5fb..94ccbe7a 100644 --- a/nistats/utils.py +++ b/nistats/utils.py @@ -41,18 +41,53 @@ def _check_and_load_tables(tables_, var_name): (var_name, type(table), table_idx)) return tables - -def _verify_events_file_uses_valid_value_separators(events_files): + +def _verify_events_file_uses_tab_separators(events_files, _not_test=True): + """ + Raises a ValueError if provided list of text based data files + (.csv, .tsv, etc) do not enforce the BIDS convention of using Tabs + as separators. + + Only scans their first row. + Does nothing if: + If the separator used is BIDS compliant. + Paths are invalid. + File(s) are not text files. + + Does not flag comma-separated-values-files for compatibility reasons; + this may change in future as commas are not BIDS compliant. + + parameters + ---------- + events_files: str, List/Tuple[str] + A single file's path or a collection of filepaths. + Files are expected to be text files. + Non-text files will raise ValueError. + + Returns + ------- + None + + Raises + ------ + ValueError: + If value separators are not Tabs (or commas) + """ valid_separators = [',', '\t'] events_files = [events_files] if isinstance(events_files, str) else events_files for events_file_ in events_files: try: with open(events_file_, 'r') as events_file_obj: - events_file_sample = events_file_obj.read(1024) - except TypeError as err: - pass + events_file_sample = events_file_obj.readline() + except TypeError: + if not _not_test: # Do nothing if events is defined in a Pandas dataframe. + raise # except when testing code. + except UnicodeDecodeError: + if not _not_test: # Do nothing in Py3 if file is binary. + raise # except when testing code. except IOError: - pass + if not _not_test: # Do nothing if filepath is invalid. + raise # except when testing code. else: try: csv.Sniffer().sniff(sample=events_file_sample, @@ -60,7 +95,8 @@ def _verify_events_file_uses_valid_value_separators(events_files): ) except csv.Error: raise ValueError( - 'The values in the events file are not separated by tabs or commas', + 'The values in the events file are not separated by tabs; ' + 'please enforce BIDS conventions', events_file_) @@ -73,9 +109,6 @@ def _check_run_tables(run_imgs, tables_, tables_name): return tables_ - - - def z_score(pvalue): """ Return the z-score corresponding to a given p-value. """ From fcd269745723a026858a3ab15b2be45e06391d62 Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Wed, 26 Sep 2018 15:56:16 +0200 Subject: [PATCH 080/210] Renamed function parameter _not_test to _raise_delimiter_errors_only - Renamed parameter _not_test in _verify_events_file_uses_tab_separators to _raise_delimiter_errors_only. --- nistats/utils.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nistats/utils.py b/nistats/utils.py index 94ccbe7a..22df6428 100644 --- a/nistats/utils.py +++ b/nistats/utils.py @@ -42,7 +42,7 @@ def _check_and_load_tables(tables_, var_name): return tables -def _verify_events_file_uses_tab_separators(events_files, _not_test=True): +def _verify_events_file_uses_tab_separators(events_files, _raise_delimiter_errors_only=True): """ Raises a ValueError if provided list of text based data files (.csv, .tsv, etc) do not enforce the BIDS convention of using Tabs @@ -80,13 +80,13 @@ def _verify_events_file_uses_tab_separators(events_files, _not_test=True): with open(events_file_, 'r') as events_file_obj: events_file_sample = events_file_obj.readline() except TypeError: - if not _not_test: # Do nothing if events is defined in a Pandas dataframe. + if not _raise_delimiter_errors_only: # Do nothing if events is defined in a Pandas dataframe. raise # except when testing code. except UnicodeDecodeError: - if not _not_test: # Do nothing in Py3 if file is binary. + if not _raise_delimiter_errors_only: # Do nothing in Py3 if file is binary. raise # except when testing code. except IOError: - if not _not_test: # Do nothing if filepath is invalid. + if not _raise_delimiter_errors_only: # Do nothing if filepath is invalid. raise # except when testing code. else: try: From 4310a882cefb23b4e1b7657d89fcc0c48d250770 Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Wed, 26 Sep 2018 16:00:12 +0200 Subject: [PATCH 081/210] Improved comments --- nistats/utils.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/nistats/utils.py b/nistats/utils.py index 22df6428..f3432e17 100644 --- a/nistats/utils.py +++ b/nistats/utils.py @@ -80,14 +80,17 @@ def _verify_events_file_uses_tab_separators(events_files, _raise_delimiter_error with open(events_file_, 'r') as events_file_obj: events_file_sample = events_file_obj.readline() except TypeError: - if not _raise_delimiter_errors_only: # Do nothing if events is defined in a Pandas dataframe. - raise # except when testing code. + # No exceptions raised if events is a Pandas dataframe except test + if not _raise_delimiter_errors_only: + raise except UnicodeDecodeError: - if not _raise_delimiter_errors_only: # Do nothing in Py3 if file is binary. + # No exceptions raised in Py3 if file is binary except for testing. + if not _raise_delimiter_errors_only: raise # except when testing code. except IOError: - if not _raise_delimiter_errors_only: # Do nothing if filepath is invalid. - raise # except when testing code. + # No exceptions raised if filepath is invalid except for testing. + if not _raise_delimiter_errors_only: + raise else: try: csv.Sniffer().sniff(sample=events_file_sample, From 0ba4344398e7eda6b3ab13a44ab9d60edd385e46 Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Wed, 26 Sep 2018 16:40:34 +0200 Subject: [PATCH 082/210] _verify_events_file_uses_tab_separators() returns a List[List[events, error]] - Param _raise_delimiter_errors_only removed --- nistats/utils.py | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/nistats/utils.py b/nistats/utils.py index f3432e17..40963af7 100644 --- a/nistats/utils.py +++ b/nistats/utils.py @@ -42,7 +42,7 @@ def _check_and_load_tables(tables_, var_name): return tables -def _verify_events_file_uses_tab_separators(events_files, _raise_delimiter_errors_only=True): +def _verify_events_file_uses_tab_separators(events_files): """ Raises a ValueError if provided list of text based data files (.csv, .tsv, etc) do not enforce the BIDS convention of using Tabs @@ -66,7 +66,11 @@ def _verify_events_file_uses_tab_separators(events_files, _raise_delimiter_error Returns ------- - None + errors_raised: List[List[(events filepath or dataframe, error]] + Possible errors: + TypeError: If events is a pandadas dataframe. + UnicodeDecodeError: If events is a binary file (Python3 only) + IOError: If events file's path is invalid Raises ------ @@ -75,22 +79,20 @@ def _verify_events_file_uses_tab_separators(events_files, _raise_delimiter_error """ valid_separators = [',', '\t'] events_files = [events_files] if isinstance(events_files, str) else events_files + errors_raised = [] for events_file_ in events_files: try: with open(events_file_, 'r') as events_file_obj: events_file_sample = events_file_obj.readline() - except TypeError: - # No exceptions raised if events is a Pandas dataframe except test - if not _raise_delimiter_errors_only: - raise - except UnicodeDecodeError: - # No exceptions raised in Py3 if file is binary except for testing. - if not _raise_delimiter_errors_only: - raise # except when testing code. - except IOError: - # No exceptions raised if filepath is invalid except for testing. - if not _raise_delimiter_errors_only: - raise + except TypeError as type_err: + # Exceptions returned, not raised, if events is a Pandas dataframe. + errors_raised.append([events_file_, type_err]) + except UnicodeDecodeError as unicode_err: + # Exceptions returned, not raised, in Py3 if file is binary except. + errors_raised.append([events_file_, unicode_err]) + except IOError as io_err: + # Exceptions returned, not raised, if filepath is invalid except. + errors_raised.append([events_file_, io_err]) else: try: csv.Sniffer().sniff(sample=events_file_sample, @@ -101,6 +103,7 @@ def _verify_events_file_uses_tab_separators(events_files, _raise_delimiter_error 'The values in the events file are not separated by tabs; ' 'please enforce BIDS conventions', events_file_) + return errors_raised def _check_run_tables(run_imgs, tables_, tables_name): From 6980749800fb934889acbc4ae515d138b6e99c85 Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Wed, 26 Sep 2018 16:44:52 +0200 Subject: [PATCH 083/210] Modified comments --- nistats/utils.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/nistats/utils.py b/nistats/utils.py index 40963af7..ceb51238 100644 --- a/nistats/utils.py +++ b/nistats/utils.py @@ -85,14 +85,11 @@ def _verify_events_file_uses_tab_separators(events_files): with open(events_file_, 'r') as events_file_obj: events_file_sample = events_file_obj.readline() except TypeError as type_err: - # Exceptions returned, not raised, if events is a Pandas dataframe. - errors_raised.append([events_file_, type_err]) + errors_raised.append([events_file_, type_err]) # events is Pandas dataframe. except UnicodeDecodeError as unicode_err: - # Exceptions returned, not raised, in Py3 if file is binary except. - errors_raised.append([events_file_, unicode_err]) + errors_raised.append([events_file_, unicode_err]) # py3:if binary file except IOError as io_err: - # Exceptions returned, not raised, if filepath is invalid except. - errors_raised.append([events_file_, io_err]) + errors_raised.append([events_file_, io_err]) # if invalid filepath. else: try: csv.Sniffer().sniff(sample=events_file_sample, From 5a407fa1147dcea33a853574eb269e10b15ed8a5 Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Thu, 27 Sep 2018 17:46:21 +0200 Subject: [PATCH 084/210] Added unit tests for nistats._utils._verify_events_file_uses_tab_separators() - If events_file param value for _verify_events_file_uses_tab_separators() is not a list or tuple, it is converted to list. --- nistats/tests/unit_tests/__init__.py | 0 .../unit_tests/test_verify_delimiters_used.py | 148 ++++++++++++++++++ nistats/utils.py | 2 +- 3 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 nistats/tests/unit_tests/__init__.py create mode 100644 nistats/tests/unit_tests/test_verify_delimiters_used.py diff --git a/nistats/tests/unit_tests/__init__.py b/nistats/tests/unit_tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/nistats/tests/unit_tests/test_verify_delimiters_used.py b/nistats/tests/unit_tests/test_verify_delimiters_used.py new file mode 100644 index 00000000..49d13dc3 --- /dev/null +++ b/nistats/tests/unit_tests/test_verify_delimiters_used.py @@ -0,0 +1,148 @@ +import csv +import os +from tempfile import NamedTemporaryFile + +import pandas as pd +from nose.tools import (assert_raises, + assert_true, + ) + +from nistats.utils import _verify_events_file_uses_tab_separators + + +def make_data_for_test_runs(): + data_for_temp_datafile = [ + ['csf', 'constant', 'linearTrend', 'wm'], + [13343.032102491035, 1.0, 0.0, 9486.199545677482], + [13329.224068063204, 1.0, 1.0, 9497.003324892803], + [13291.755627241291, 1.0, 2.0, 9484.012965365506], + ] + + delimiters = { + 'tab': '\t', + 'comma': ',', + 'space': ' ', + 'semicolon': ';', + 'hyphen': '-', + } + + return data_for_temp_datafile, delimiters + + +def _create_test_file(temp_csv, test_data, delimiter): + csv_writer = csv.writer(temp_csv, delimiter=delimiter) + for row in test_data: + csv_writer.writerow(row) + temp_csv.flush() + + +def _run_test_for_invalid_separator(filepath, delimiter_name): + if delimiter_name not in ('tab', 'comma'): + with assert_raises(ValueError): + _verify_events_file_uses_tab_separators(events_files=filepath) + else: + result = _verify_events_file_uses_tab_separators(events_files=filepath) + assert_true(result == []) + + +def test_for_invalid_separator(): + data_for_temp_datafile, delimiters = make_data_for_test_runs() + for delimiter_name, delimiter_char in delimiters.items(): + tmp_file_prefix, temp_file_suffix = ( + 'tmp ', ' ' + delimiter_name + '.csv') + with NamedTemporaryFile(mode='w', dir=os.getcwd(), + prefix=tmp_file_prefix, + suffix=temp_file_suffix) as temp_csv_obj: + _create_test_file(temp_csv=temp_csv_obj, + test_data=data_for_temp_datafile, + delimiter=delimiter_char) + _run_test_for_invalid_separator(filepath=temp_csv_obj.name, + delimiter_name=delimiter_name) + + +def test_with_2D_dataframe(): + data_for_pandas_dataframe, _ = make_data_for_test_runs() + events_pandas_dataframe = pd.DataFrame(data_for_pandas_dataframe) + result = _verify_events_file_uses_tab_separators( + events_files=events_pandas_dataframe) + expected_error = result[0][1] + with assert_raises(TypeError): + raise expected_error + + +def test_with_1D_dataframe(): + data_for_pandas_dataframe, _ = make_data_for_test_runs() + for dataframe_ in data_for_pandas_dataframe: + events_pandas_dataframe = pd.DataFrame(dataframe_) + result = _verify_events_file_uses_tab_separators( + events_files=events_pandas_dataframe) + expected_error = result[0][1] + with assert_raises(TypeError): + raise expected_error + + +def test_for_invalid_filepath(): + filepath = 'junk_file_path.csv' + result = _verify_events_file_uses_tab_separators(events_files=filepath) + expected_error = result[0][1] + with assert_raises(IOError): + raise expected_error + + +def test_for_pandas_dataframe(): + events_pandas_dataframe = pd.DataFrame([['a', 'b', 'c'], [0, 1, 2]]) + result = _verify_events_file_uses_tab_separators( + events_files=events_pandas_dataframe) + expected_error = result[0][1] + with assert_raises(TypeError): + raise expected_error + + +def test_binary_opening_an_image(): + img_data = bytearray( + b'GIF87a\x01\x00\x01\x00\xe7*\x00\x00\x00\x00\x01\x01\x01\x02\x02' + b'\x07\x08\x08\x08\t\t\t\n\n\n\x0b\x0b\x0b\x0c\x0c\x0c\r;') + with NamedTemporaryFile(mode='wb', suffix='.gif', + dir=os.getcwd()) as temp_img_obj: + temp_img_obj.write(img_data) + with assert_raises(ValueError): + _verify_events_file_uses_tab_separators( + events_files=temp_img_obj.name) + + +def test_binary_bytearray_of_ints_data(): + temp_data_bytearray_from_ints = bytearray([0, 1, 0, 11, 10]) + with NamedTemporaryFile(mode='wb', dir=os.getcwd(), + suffix='.bin') as temp_bin_obj: + temp_bin_obj.write(temp_data_bytearray_from_ints) + with assert_raises(ValueError): + result = _verify_events_file_uses_tab_separators( + events_files=temp_bin_obj.name) + + +if __name__ == '__main__': + + def _run_tests_print_test_messages(test_func): + from pprint import pprint + pprint(['Running', test_func.__name__]) + test_func() + pprint('... complete') + + + def run_test_suite(): + tests = [ + test_for_invalid_filepath, + test_with_2D_dataframe, + test_with_1D_dataframe, + test_for_invalid_filepath, + test_for_pandas_dataframe, + test_binary_opening_an_image, + test_binary_bytearray_of_ints_data, + ] + + for test_ in tests: + _run_tests_print_test_messages(test_func=test_) + + + + run_test_suite() diff --git a/nistats/utils.py b/nistats/utils.py index ceb51238..29a7feb9 100644 --- a/nistats/utils.py +++ b/nistats/utils.py @@ -78,7 +78,7 @@ def _verify_events_file_uses_tab_separators(events_files): If value separators are not Tabs (or commas) """ valid_separators = [',', '\t'] - events_files = [events_files] if isinstance(events_files, str) else events_files + events_files = [events_files] if not isinstance(events_files, (list, tuple)) else events_files errors_raised = [] for events_file_ in events_files: try: From 1bd02e25fa8b06759160ea889678cef25ee36370 Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Thu, 27 Sep 2018 18:01:12 +0200 Subject: [PATCH 085/210] Corrected unit test filename to test_verify_events_file_uses_tab_separators - Renamed test_verify_delimiters_used -> test_verify_events_file_uses_tab_separators. - Fixed incorrect indentation in ain.run_test_suite() . --- ...sed.py => test_verify_events_file_uses_tab_separators.py} | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) rename nistats/tests/unit_tests/{test_verify_delimiters_used.py => test_verify_events_file_uses_tab_separators.py} (98%) diff --git a/nistats/tests/unit_tests/test_verify_delimiters_used.py b/nistats/tests/unit_tests/test_verify_events_file_uses_tab_separators.py similarity index 98% rename from nistats/tests/unit_tests/test_verify_delimiters_used.py rename to nistats/tests/unit_tests/test_verify_events_file_uses_tab_separators.py index 49d13dc3..147c9c44 100644 --- a/nistats/tests/unit_tests/test_verify_delimiters_used.py +++ b/nistats/tests/unit_tests/test_verify_events_file_uses_tab_separators.py @@ -139,9 +139,8 @@ def run_test_suite(): test_binary_opening_an_image, test_binary_bytearray_of_ints_data, ] - - for test_ in tests: - _run_tests_print_test_messages(test_func=test_) + for test_ in tests: + _run_tests_print_test_messages(test_func=test_) From 345517269cdd77c04d8868bd8bba6c7ea773bd75 Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Mon, 1 Oct 2018 11:06:20 +0200 Subject: [PATCH 086/210] Moved test file from nistats/tests/unit_tests/ to nistats/tests/ --- .../test_verify_events_file_uses_tab_separators.py | 0 nistats/tests/unit_tests/__init__.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename nistats/tests/{unit_tests => }/test_verify_events_file_uses_tab_separators.py (100%) delete mode 100644 nistats/tests/unit_tests/__init__.py diff --git a/nistats/tests/unit_tests/test_verify_events_file_uses_tab_separators.py b/nistats/tests/test_verify_events_file_uses_tab_separators.py similarity index 100% rename from nistats/tests/unit_tests/test_verify_events_file_uses_tab_separators.py rename to nistats/tests/test_verify_events_file_uses_tab_separators.py diff --git a/nistats/tests/unit_tests/__init__.py b/nistats/tests/unit_tests/__init__.py deleted file mode 100644 index e69de29b..00000000 From 0c2c9ebde884f506fabb3cb236094a9ca7326d42 Mon Sep 17 00:00:00 2001 From: bthirion Date: Thu, 13 Sep 2018 14:25:49 +0200 Subject: [PATCH 087/210] Started populating the branch --- .../plot_first_level_model_details.py | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 examples/01_tutorials/plot_first_level_model_details.py diff --git a/examples/01_tutorials/plot_first_level_model_details.py b/examples/01_tutorials/plot_first_level_model_details.py new file mode 100644 index 00000000..06e6aafb --- /dev/null +++ b/examples/01_tutorials/plot_first_level_model_details.py @@ -0,0 +1,35 @@ +"""Studying firts-level-model details in a trials-and-error fashion +================================================================ + +In this tutorial, we study the parametrization of the first-level +model used for fMRI data analysis and clarify their impact on the +results of the analysis. + +We use an exploratory approach, in which we incrementally include some +new features in the analysis and look at the outcome, i.e. the +resulting brain maps. + +Readers without prior experience in fMRI data analysis should first +run the plot_sing_subject_single_run tutorial to get a bit more +familiar with the base concepts, and only then run thi script. + +To run this example, you must launch IPython via ``ipython +--matplotlib`` in a terminal, or use ``jupyter-notebook``. + +.. contents:: **Contents** + :local: + :depth: 1 + +""" + +############################################################################### +# Retrieving the data +# ------------------- +# +# We use a so-called localizer dataset, which consists in a 5-minutes +# acquisition of a fast event-related dataset. + + +############################################################################### +# Running a base model +# ------------------- From 7e3a92803c8fa9f166d3c5ff370ba9527777b5e7 Mon Sep 17 00:00:00 2001 From: bthirion Date: Thu, 13 Sep 2018 23:47:56 +0200 Subject: [PATCH 088/210] Improved the script --- .../plot_first_level_model_details.py | 84 ++++++++++++++++++- 1 file changed, 80 insertions(+), 4 deletions(-) diff --git a/examples/01_tutorials/plot_first_level_model_details.py b/examples/01_tutorials/plot_first_level_model_details.py index 06e6aafb..509484f1 100644 --- a/examples/01_tutorials/plot_first_level_model_details.py +++ b/examples/01_tutorials/plot_first_level_model_details.py @@ -22,14 +22,90 @@ """ +import numpy as np +import pandas as pd +from nilearn import plotting +from nistats.first_level_model import FirstLevelModel +from nistats import datasets + ############################################################################### # Retrieving the data # ------------------- # # We use a so-called localizer dataset, which consists in a 5-minutes -# acquisition of a fast event-related dataset. +# acquisition of a fast event-related dataset. +subject_data = datasets.fetch_spm_multimodal_fmri() +tr = 2. +from nilearn.image import concat_imgs, mean_img, threshold_img, crop_img +fmri_img = concat_imgs(subject_data.func1, auto_resample=True) -############################################################################### -# Running a base model -# ------------------- +######################################################################### +# Create mean image for display +mean_image = mean_img(fmri_img) +bg_image = crop_img(threshold_img(mean_image, 66)) + +######################################################################### +# Get the experimental paradigm +n_scans = fmri_img.shape[-1] +from scipy.io import loadmat +timing = loadmat(getattr(subject_data, "trials_ses1"), + squeeze_me=True, struct_as_record=False) +faces_onsets = timing['onsets'][0].ravel() +scrambled_onsets = timing['onsets'][1].ravel() +onsets = np.hstack((faces_onsets, scrambled_onsets)) +onsets *= tr # because onsets were reporting in 'scans' units +conditions = (['faces'] * len(faces_onsets) + + ['scrambled'] * len(scrambled_onsets)) +paradigm = pd.DataFrame({'trial_type': conditions, 'onset': onsets}) + +# Build design matrix +frame_times = np.arange(n_scans) * tr +from nistats.design_matrix import make_design_matrix +design_matrix = make_design_matrix(frame_times, paradigm) + +######################################################################### +# We can specify some contrasts (To get corresponding maps) +# for the sake of script concision, it is advatageous to make it a function + +def make_contrasts(design_matrix): + contrast_matrix = np.eye(design_matrix.shape[1]) + contrasts = dict([(column, contrast_matrix[i]) + for i, column in enumerate(design_matrix.columns)]) + return{ + 'faces-scrambled': contrasts['faces'] - contrasts['scrambled'], + 'scrambled-faces': -contrasts['faces'] + contrasts['scrambled'], + 'effects_of_interest': np.vstack((contrasts['faces'], + contrasts['scrambled'])) + } + +contrasts = make_contrasts(design_matrix) + +######################################################################### +# Fit GLM +print('Fitting a GLM') +fmri_glm = FirstLevelModel(tr) +fmri_glm = fmri_glm.fit(fmri_img, design_matrices=design_matrix) + +######################################################################### +# Compute contrast maps +# + +from nilearn import plotting +import matplotlib.pyplot as plt + +plt.figure(figsize=(8, 2.5)) +for i, (contrast_id, contrast_val) in enumerate(contrasts.items()): + ax = plt.subplot(1, len(contrasts), i + 1) + z_map = fmri_glm.compute_contrast( + contrast_val, output_type='z_score') + plotting.plot_stat_map( + z_map, bg_img=bg_image, threshold=3.0, display_mode='z', vmax=7, + black_bg=True, title=contrast_id, axes=ax, cut_coords=[0]) + +plotting.show() + + +######################################################################### +# let's explore now wome variants around this bbasic model +# From 6993b684ed51c2148d3cf2ce7d6c1346484af965 Mon Sep 17 00:00:00 2001 From: bthirion Date: Sat, 15 Sep 2018 22:59:25 +0200 Subject: [PATCH 089/210] Added trials-and error on hrf model --- .../plot_first_level_model_details.py | 281 ++++++++++++++---- 1 file changed, 231 insertions(+), 50 deletions(-) diff --git a/examples/01_tutorials/plot_first_level_model_details.py b/examples/01_tutorials/plot_first_level_model_details.py index 509484f1..c782ef4f 100644 --- a/examples/01_tutorials/plot_first_level_model_details.py +++ b/examples/01_tutorials/plot_first_level_model_details.py @@ -25,7 +25,7 @@ import numpy as np import pandas as pd from nilearn import plotting -from nistats.first_level_model import FirstLevelModel + from nistats import datasets ############################################################################### @@ -35,77 +35,258 @@ # We use a so-called localizer dataset, which consists in a 5-minutes # acquisition of a fast event-related dataset. -subject_data = datasets.fetch_spm_multimodal_fmri() -tr = 2. -from nilearn.image import concat_imgs, mean_img, threshold_img, crop_img -fmri_img = concat_imgs(subject_data.func1, auto_resample=True) +data = datasets.fetch_localizer_first_level() +t_r = 2.4 +paradigm_file = data.paradigm +events= pd.read_csv(paradigm_file, sep=' ', header=None, index_col=None) +events.columns = ['session', 'trial_type', 'onset'] +fmri_img = data.epi_img -######################################################################### -# Create mean image for display -mean_image = mean_img(fmri_img) -bg_image = crop_img(threshold_img(mean_image, 66)) +############################################################################### +# Running a basic model +# --------------------- -######################################################################### -# Get the experimental paradigm -n_scans = fmri_img.shape[-1] -from scipy.io import loadmat -timing = loadmat(getattr(subject_data, "trials_ses1"), - squeeze_me=True, struct_as_record=False) -faces_onsets = timing['onsets'][0].ravel() -scrambled_onsets = timing['onsets'][1].ravel() -onsets = np.hstack((faces_onsets, scrambled_onsets)) -onsets *= tr # because onsets were reporting in 'scans' units -conditions = (['faces'] * len(faces_onsets) + - ['scrambled'] * len(scrambled_onsets)) -paradigm = pd.DataFrame({'trial_type': conditions, 'onset': onsets}) +from nistats.first_level_model import FirstLevelModel +first_level_model = FirstLevelModel(t_r) +first_level_model = first_level_model.fit(fmri_img, events=events) +design_matrix = first_level_model.design_matrices_[0] -# Build design matrix -frame_times = np.arange(n_scans) * tr -from nistats.design_matrix import make_design_matrix -design_matrix = make_design_matrix(frame_times, paradigm) +from nistats.reporting import plot_design_matrix +plot_design_matrix(design_matrix) ######################################################################### -# We can specify some contrasts (To get corresponding maps) -# for the sake of script concision, it is advatageous to make it a function +# Specify the contrasts. +# +# For this, let's create a function that, given the deisgn matrix, +# generates the corresponding contrasts. +# This will be useful -def make_contrasts(design_matrix): +def make_localizer_contrasts(design_matrix): + """ returns a dictionary of four contasts, given the design matrix""" + # first generate canonical contrasts contrast_matrix = np.eye(design_matrix.shape[1]) contrasts = dict([(column, contrast_matrix[i]) for i, column in enumerate(design_matrix.columns)]) - return{ - 'faces-scrambled': contrasts['faces'] - contrasts['scrambled'], - 'scrambled-faces': -contrasts['faces'] + contrasts['scrambled'], - 'effects_of_interest': np.vstack((contrasts['faces'], - contrasts['scrambled'])) + # Add more complex contrasts + contrasts["audio"] = contrasts["clicDaudio"] + contrasts["clicGaudio"] +\ + contrasts["calculaudio"] + contrasts["phraseaudio"] + contrasts["video"] = contrasts["clicDvideo"] + contrasts["clicGvideo"] + \ + contrasts["calculvideo"] + contrasts["phrasevideo"] + contrasts["computation"] = contrasts["calculaudio"] + contrasts["calculvideo"] + contrasts["sentences"] = contrasts["phraseaudio"] + contrasts["phrasevideo"] + + ######################################################################### + # Short list of more relevant contrasts + contrasts = { + "left-right": (contrasts["clicGaudio"] + contrasts["clicGvideo"] + - contrasts["clicDaudio"] - contrasts["clicDvideo"]), + "H-V": contrasts["damier_H"] - contrasts["damier_V"], + "audio-video": contrasts["audio"] - contrasts["video"], + "computation-sentences": (contrasts["computation"] - + contrasts["sentences"]), } + return contrasts + +contrasts = make_localizer_contrasts(design_matrix) +# TODO: plot contrasts + +######################################################################### +# contrast estimation and plotting +# Since this script will be repeated several times, for the sake of readbility, +# we encapsulate it in a function that we call when needed. +# + +import matplotlib.pyplot as plt + +def plot_contrast(first_level_model): + """ Given a first model, specify, enstimate and plot the main contrasts""" + design_matrix = first_level_model.design_matrices_[0] + # Call the contrast specification within the function + contrasts = make_localizer_contrasts(design_matrix) + fig = plt.figure(figsize=(11, 3)) + for index, (contrast_id, contrast_val) in enumerate(contrasts.items()): + ax = plt.subplot(1, len(contrasts), 1 + index) + z_map = first_level_model.compute_contrast( + contrast_val, output_type='z_score') + plotting.plot_stat_map( + z_map, display_mode='z', threshold=3.0, title=contrast_id, axes=ax, + cut_coords=1) + +plt.show() -contrasts = make_contrasts(design_matrix) +######################################################################### +# Changing the drift model +# ------------------------ +# +# By default the drift model is a set of slow oscillating functions (Discrete Cosine transform), with a cutoff at frequency 1/128 hz. +# We can change this cut-off, e.g. to 1/64Hz. +# This is done by setting period_cut=64(s) + +first_level_model = FirstLevelModel(t_r, period_cut=64) +first_level_model = first_level_model.fit(fmri_img, events=events) +design_matrix = first_level_model.design_matrices_[0] +plot_design_matrix(design_matrix) ######################################################################### -# Fit GLM -print('Fitting a GLM') -fmri_glm = FirstLevelModel(tr) -fmri_glm = fmri_glm.fit(fmri_img, design_matrices=design_matrix) +# Does the model perform worse or better ? + +plot_contrast(first_level_model) +plt.show() ######################################################################### -# Compute contrast maps +# Note that the design matrix has more columns to model dirft terms +# +# Anyway, this model performs rather poorly # +# Another solution is to remove these drift terms. Maybe they're simply useless. +# this is done by setting drift_model to None. -from nilearn import plotting -import matplotlib.pyplot as plt +first_level_model = FirstLevelModel(t_r, drift_model=None) +first_level_model = first_level_model.fit(fmri_img, events=events) +design_matrix = first_level_model.design_matrices_[0] +plot_design_matrix(design_matrix) +plot_contrast(first_level_model) +plt.show() + +######################################################################### +# Is it better than the original ? No ! +# +# Note that the design matrix has changed with no drift columns. +# the event columns, on the other hand, haven't changed. +# Another alternative to get a drift model is to specify a set of polynomials +# Let's take a basis of 5 polynomials + +first_level_model = FirstLevelModel(t_r, drift_model='polynomial', drift_order=5) +first_level_model = first_level_model.fit(fmri_img, events=events) +design_matrix = first_level_model.design_matrices_[0] +plot_design_matrix(design_matrix) +plot_contrast(first_level_model) +plt.show() + +######################################################################### +# Is it good ? No better, no worse. Let's turn to another parameter. + +######################################################################### +# Changing the hemodynamic response model +# --------------------------------------- +# +# This is the filter used to convert the event sequence into a reference BOLD signal for the design matrix. +# +# The first thing that we can do is to change the default model (the so-called Glover hrf) for the so-called canocial model of SPM --which has slightly weaker undershoot component. + +first_level_model = FirstLevelModel(t_r, hrf_model='spm') +first_level_model = first_level_model.fit(fmri_img, events=events) +design_matrix = first_level_model.design_matrices_[0] +plot_design_matrix(design_matrix) +plot_contrast(first_level_model) +plt.show() + +######################################################################### +# No strong --positive or negative-- effect. +# +# We could try to go one step further: using not only the so-called canocial hrf, but also its time derivative. Note that in that case, we still perform the contrasts and obtain statistical significance for the main effect ---not the time derivative. This means that the inclusion of time derivative in the design matrix has the sole effect of discounting timing misspecification from the error term, which vould decrease the estimated variance and enhance the statistical significance of the effect. Is it the case ? + +first_level_model = FirstLevelModel(t_r, hrf_model='spm + derivative') +first_level_model = first_level_model.fit(fmri_img, events=events) +design_matrix = first_level_model.design_matrices_[0] +plot_design_matrix(design_matrix) +plot_contrast(first_level_model) +plt.show() + +######################################################################### +# Not a huge effect, but rather positive overall. We could keep that one +# +# Bzw, a benefit of this approach is that we can test which voxels are well explined by the derivative term, hinting at misfit regions, a possibly valuable information +# This is implemented by an F test on the time derivative regressors. + +contrast_val = np.eye(design_matrix.shape[1])[1:2:21] +z_map = first_level_model.compute_contrast( + contrast_val, output_type='z_score') +plotting.plot_stat_map( + z_map, display_mode='z', threshold=3.0, title="effect of time derivatives") +plt.show() + +######################################################################### +# We don't see too much here: the onset times and hrf delay we're using are probably fine. + +######################################################################### +# But we could also add the so-called dispersion derivative, to dicount hrf shape differeneces. Let's give a try +# Once again, we're only going to test the main regressor, not the dispersion derivative. This new model only changes the variance term. + +first_level_model = FirstLevelModel(t_r, hrf_model='spm + derivative + dispersion') +first_level_model = first_level_model.fit(fmri_img, events=events) +design_matrix = first_level_model.design_matrices_[0] +plot_design_matrix(design_matrix) +plot_contrast(first_level_model) +plt.show() + +######################################################################### +# There are some milde effects. Maybe not worth the complexity increase ! +# +# Next solution is to try fininte impulse reponse (FIR) models: we just say that the hrf is an arbitrary function that lags behind the stimulus onset. +# In the present case, given that the numbers of condition is high, we should use a simple FIR model. +# +# Concretely, we set `hrf_model` to 'fir' and `fir_delays` to [3, 5, 7] (s) + + +first_level_model = FirstLevelModel(t_r, hrf_model='fir', fir_delays=[3, 5, 7]) +first_level_model = first_level_model.fit(fmri_img, events=events) +design_matrix = first_level_model.design_matrices_[0] +plot_design_matrix(design_matrix) +plt.show() + +######################################################################### +# We have to change the contrast specification. We characterize the BOLD reposne by the sum across the three time lags. It's a bit hairy, sorry, but this is the price to pay for flexibility... + +contrast_matrix = np.eye(design_matrix.shape[1]) +contrasts = dict([(column, contrast_matrix[i]) + for i, column in enumerate(design_matrix.columns)]) +conditions = events.trial_type.unique() +for condition in conditions: + contrasts[condition] = np.sum( + [contrasts[name] for name in design_matrix.columns + if name[:len(condition)] == condition], 0) -plt.figure(figsize=(8, 2.5)) -for i, (contrast_id, contrast_val) in enumerate(contrasts.items()): - ax = plt.subplot(1, len(contrasts), i + 1) - z_map = fmri_glm.compute_contrast( +contrasts["audio"] = np.sum( + [contrasts[name] for name in + ["clicDaudio", "clicGaudio", "calculaudio", "phraseaudio"]], 0) +contrasts["video"] = np.sum( + [contrasts[name] for name in + ["clicDvideo", "clicGvideo", "calculvideo", "phrasevideo"]], 0) +contrasts["computation"] = contrasts["calculaudio"] + contrasts["calculvideo"] +contrasts["sentences"] = contrasts["phraseaudio"] + contrasts["phrasevideo"] + +contrasts = { + "left-right": (contrasts["clicGaudio"] + contrasts["clicGvideo"] + - contrasts["clicDaudio"] - contrasts["clicDvideo"]), + "H-V": contrasts["damier_H"] - contrasts["damier_V"], + "audio-video": contrasts["audio"] - contrasts["video"], + "computation-sentences": (contrasts["computation"] - + contrasts["sentences"]), + } + +######################################################################### +# Take a breathe. +# +# We can now proceed by estimating the contrasts and displaying them. + + +fig = plt.figure(figsize=(11, 3)) +for index, (contrast_id, contrast_val) in enumerate(contrasts.items()): + ax = plt.subplot(1, len(contrasts), 1 + index) + z_map = first_level_model.compute_contrast( contrast_val, output_type='z_score') plotting.plot_stat_map( - z_map, bg_img=bg_image, threshold=3.0, display_mode='z', vmax=7, - black_bg=True, title=contrast_id, axes=ax, cut_coords=[0]) + z_map, display_mode='z', threshold=3.0, title=contrast_id, axes=ax, + cut_coords=1) -plotting.show() +######################################################################### +# The result is not convincing to my eyes. Maybe we're asking a bit too much to a small dataset, with a relatively large number of experimental conditions! +# ######################################################################### -# let's explore now wome variants around this bbasic model +# Conclusion +# ---------- # +# Interestingly, the model used here seems quite resilient to manipulation of modeling parameters: this is reassuing. It shows that Nistats defaults ('cosine' drift, cutoff=128s, 'glover' hrf) are actually reasonable. From c833e49c3584af01ba520c0aaa585e1716785262 Mon Sep 17 00:00:00 2001 From: bthirion Date: Sat, 15 Sep 2018 23:45:17 +0200 Subject: [PATCH 090/210] completed script, removed FIR part --- .../plot_first_level_model_details.py | 81 ++++++------------- 1 file changed, 23 insertions(+), 58 deletions(-) diff --git a/examples/01_tutorials/plot_first_level_model_details.py b/examples/01_tutorials/plot_first_level_model_details.py index c782ef4f..572155a2 100644 --- a/examples/01_tutorials/plot_first_level_model_details.py +++ b/examples/01_tutorials/plot_first_level_model_details.py @@ -210,10 +210,12 @@ def plot_contrast(first_level_model): # We don't see too much here: the onset times and hrf delay we're using are probably fine. ######################################################################### -# But we could also add the so-called dispersion derivative, to dicount hrf shape differeneces. Let's give a try -# Once again, we're only going to test the main regressor, not the dispersion derivative. This new model only changes the variance term. +# The noise model ar(1) or ols ? +# ------------------------------ +# +# So far,we have implitly use an lag-1 autoregressive model ---aka ar(1)--- for the temporal structure of the noise. an alternative choice is to use an ordinaly least sqaure model (ols) that neglects that assumes no temporal structure (independent noise) -first_level_model = FirstLevelModel(t_r, hrf_model='spm + derivative + dispersion') +first_level_model = FirstLevelModel(t_r, hrf_model='spm + derivative', noise_model='ols') first_level_model = first_level_model.fit(fmri_img, events=events) design_matrix = first_level_model.design_matrices_[0] plot_design_matrix(design_matrix) @@ -221,72 +223,35 @@ def plot_contrast(first_level_model): plt.show() ######################################################################### -# There are some milde effects. Maybe not worth the complexity increase ! -# -# Next solution is to try fininte impulse reponse (FIR) models: we just say that the hrf is an arbitrary function that lags behind the stimulus onset. -# In the present case, given that the numbers of condition is high, we should use a simple FIR model. -# -# Concretely, we set `hrf_model` to 'fir' and `fir_delays` to [3, 5, 7] (s) +# While the difference is not obvious you should rather stick to the ar(1) model, which is arguably more accurate. +######################################################################### +# Removing confounds +# ------------------ +# +# A problematic feature of fMRI is the presence of unconctrolled confounds in the data, sue to scanner instabilities (spikes) or physiological phenomena (motion, heart and repoiration rate) +# Side measurements are sometimes acquired to charcterise these effects. We don't have access to those. +# What we can do instead id to estimate confounding effects from the data themselves, using the compcorr approach, and take those nto account in the model -first_level_model = FirstLevelModel(t_r, hrf_model='fir', fir_delays=[3, 5, 7]) -first_level_model = first_level_model.fit(fmri_img, events=events) +from nilearn.image import high_variance_confounds +confounds = pd.DataFrame(high_variance_confounds(fmri_img, percentile=1)) +first_level_model = FirstLevelModel(t_r, hrf_model='spm + derivative') +first_level_model = first_level_model.fit(fmri_img, events=events, + confounds=confounds) design_matrix = first_level_model.design_matrices_[0] plot_design_matrix(design_matrix) +plot_contrast(first_level_model) plt.show() ######################################################################### -# We have to change the contrast specification. We characterize the BOLD reposne by the sum across the three time lags. It's a bit hairy, sorry, but this is the price to pay for flexibility... - -contrast_matrix = np.eye(design_matrix.shape[1]) -contrasts = dict([(column, contrast_matrix[i]) - for i, column in enumerate(design_matrix.columns)]) -conditions = events.trial_type.unique() -for condition in conditions: - contrasts[condition] = np.sum( - [contrasts[name] for name in design_matrix.columns - if name[:len(condition)] == condition], 0) - -contrasts["audio"] = np.sum( - [contrasts[name] for name in - ["clicDaudio", "clicGaudio", "calculaudio", "phraseaudio"]], 0) -contrasts["video"] = np.sum( - [contrasts[name] for name in - ["clicDvideo", "clicGvideo", "calculvideo", "phrasevideo"]], 0) -contrasts["computation"] = contrasts["calculaudio"] + contrasts["calculvideo"] -contrasts["sentences"] = contrasts["phraseaudio"] + contrasts["phrasevideo"] - -contrasts = { - "left-right": (contrasts["clicGaudio"] + contrasts["clicGvideo"] - - contrasts["clicDaudio"] - contrasts["clicDvideo"]), - "H-V": contrasts["damier_H"] - contrasts["damier_V"], - "audio-video": contrasts["audio"] - contrasts["video"], - "computation-sentences": (contrasts["computation"] - - contrasts["sentences"]), - } - -######################################################################### -# Take a breathe. -# -# We can now proceed by estimating the contrasts and displaying them. - - -fig = plt.figure(figsize=(11, 3)) -for index, (contrast_id, contrast_val) in enumerate(contrasts.items()): - ax = plt.subplot(1, len(contrasts), 1 + index) - z_map = first_level_model.compute_contrast( - contrast_val, output_type='z_score') - plotting.plot_stat_map( - z_map, display_mode='z', threshold=3.0, title=contrast_id, axes=ax, - cut_coords=1) - -######################################################################### -# The result is not convincing to my eyes. Maybe we're asking a bit too much to a small dataset, with a relatively large number of experimental conditions! +# Note the five additional columns in the design matrix # +# The effect on activation maps is complex: auditory/visual effects are killed, probably because they were somewhat colinear to the confounds. On the other hand, some of the maps become cleaner (H-V, computation) after this effects. ######################################################################### # Conclusion # ---------- # -# Interestingly, the model used here seems quite resilient to manipulation of modeling parameters: this is reassuing. It shows that Nistats defaults ('cosine' drift, cutoff=128s, 'glover' hrf) are actually reasonable. +# Interestingly, the model used here seems quite resilient to manipulation of modeling parameters: this is reassuring. It shows that Nistats defaults ('cosine' drift, cutoff=128s, 'glover' hrf, ar(1) model) are actually reasonable. +# Note that these conclusions are specific to this dataset and may vary with other ones. From 8c7a79797e810be97cbd1538ed530477fa8b7dc6 Mon Sep 17 00:00:00 2001 From: bthirion Date: Wed, 19 Sep 2018 23:57:34 +0200 Subject: [PATCH 091/210] small improvement to better illustrate masking --- .../plot_first_level_model_details.py | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/examples/01_tutorials/plot_first_level_model_details.py b/examples/01_tutorials/plot_first_level_model_details.py index 572155a2..fc479342 100644 --- a/examples/01_tutorials/plot_first_level_model_details.py +++ b/examples/01_tutorials/plot_first_level_model_details.py @@ -249,6 +249,59 @@ def plot_contrast(first_level_model): # The effect on activation maps is complex: auditory/visual effects are killed, probably because they were somewhat colinear to the confounds. On the other hand, some of the maps become cleaner (H-V, computation) after this effects. +######################################################################### +# Smoothing +# ---------- +# +# Smoothing is a regularization of the model. It has two benefits: decrease the noise level in images, and reduce the discrepancy between individuals. The drawback is that it biases the shape and position of activation. +# We simply illustrate here the statistical gains. +# We use a mild smoothing of 5mm full-width at half maximum (fwhm). + +first_level_model = FirstLevelModel( + t_r, hrf_model='spm + derivative', smoothing_fwhm=5).fit( + fmri_img, events=events, confounds=confounds) +design_matrix = first_level_model.design_matrices_[0] +plot_design_matrix(design_matrix) +plot_contrast(first_level_model) +plt.show() + +######################################################################### +# The design is unchanged but the maps are smoother and more contrasted +# + +######################################################################### +# Masking +# -------- +# Masking consists in selecting the region of the image on which the model is run: it is useless to run it outside of the brain. +# the approach taken by FirstLeveModel is to estimate it from the fMRI data themselves when no mask is explicitly provided. +# Since the data have been resampled into MNI space, we can use instead a mask of the grey matter in MNI space. The benefit is that it makes voxel-level comparisons easier across subjects and datasets, and removed non-grey matter regions, in which no BOLD signal is expected. +# The down side is that the mask may not fit very well these particular data + +from nilearn.plotting import plot_roi +from nilearn.datasets import fetch_icbm152_brain_gm_mask +icbm_mask = fetch_icbm152_brain_gm_mask() +data_mask = first_level_model.masker_.mask_img_ +plt.figure(figsize=(16, 4)) +ax = plt.subplot(121) +plot_roi(icbm_mask, title='ICBM mask', axes=ax) +ax = plt.subplot(122) +plot_roi(data_mask, title='Data-driven mask', axes=ax) +plt.show() + +######################################################################### +# Impact on the first-level model + + +first_level_model = FirstLevelModel( + t_r, hrf_model='spm + derivative', smoothing_fwhm=5).fit( + fmri_img, events=events, confounds=confounds) +design_matrix = first_level_model.design_matrices_[0] +plot_design_matrix(design_matrix) +plot_contrast(first_level_model) +plt.show() + + + ######################################################################### # Conclusion # ---------- From 94d36c6700e9dfb36fd7f0533251b33403741d63 Mon Sep 17 00:00:00 2001 From: bthirion Date: Mon, 1 Oct 2018 15:35:41 +0200 Subject: [PATCH 092/210] Some cleaning in the docs --- .../plot_single_subject_single_run.py | 38 ++++++++++++------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/examples/01_tutorials/plot_single_subject_single_run.py b/examples/01_tutorials/plot_single_subject_single_run.py index 21f7713d..e1386476 100644 --- a/examples/01_tutorials/plot_single_subject_single_run.py +++ b/examples/01_tutorials/plot_single_subject_single_run.py @@ -40,6 +40,8 @@ # .. note:: In this tutorial, we load the data using a data downloading # function. To input your own data, you will need to provide # a list of paths to your own files in the ``subject_data`` variable. +# These should abide to the Brain Imaging Data Structure (BIDS) +# organization. from nistats.datasets import fetch_spm_auditory subject_data = fetch_spm_auditory() @@ -53,7 +55,7 @@ ############################################################################### # Next, we concatenate all the 3D EPI image into a single 4D image, -# the we average them in order to create a background +# then we average them in order to create a background # image that will be used to display the activations: from nilearn.image import concat_imgs, mean_img @@ -103,7 +105,7 @@ # Performing the GLM analysis # --------------------------- # -# It is now time to create and estimate a ``FirstLevelModel`` object, that will generate the *design matrix* using the information provided by the ``events` object. +# It is now time to create and estimate a ``FirstLevelModel`` object, that will generate the *design matrix* using the information provided by the ``events`` object. from nistats.first_level_model import FirstLevelModel @@ -116,6 +118,7 @@ # * hrf_model='spm' means that we rely on the SPM "canonical hrf" model (without time or dispersion derivatives) # * drift_model='cosine' means that we model the signal drifts as slow oscillating time functions # * period_cut=160(s) defines the cutoff frequency (its inverse actually). +# fmri_glm = FirstLevelModel(t_r=7, noise_model='ar1', @@ -132,15 +135,15 @@ # One can inspect the design matrix (rows represent time, and # columns contain the predictors): -from nistats.reporting import plot_design_matrix design_matrix = fmri_glm.design_matrices_[0] ############################################################################### -# We have taken the first design matrix, because the model is meant -# for multiple runs +# Formally, we have taken the first design matrix, because the model is +# implictily meant to for multiple runs. -import matplotlib.pyplot as plt +from nistats.reporting import plot_design_matrix plot_design_matrix(design_matrix) +import matplotlib.pyplot as plt plt.show() ############################################################################### @@ -174,21 +177,28 @@ active_minus_rest = conditions['active'] - conditions['rest'] ############################################################################### -# below, we compute the estimated effect. It is in BOLD signal unit, but has no statistical guarantees, because it does not take into account the associated variance. +# Let's look at it + +from nistats.reporting import plot_contrast_matrix + +plot_contrast_matrix(active_minus_rest, design_matrix=design_matrix) + +############################################################################### +# Below, we compute the estimated effect. It is in BOLD signal unit, but has no statistical guarantees, because it does not take into account the associated variance. eff_map = fmri_glm.compute_contrast(active_minus_rest, output_type='effect_size') ############################################################################### -# In order to get statistical significance, we form a t-statistic, and directly convert is into z-scale. The z-scale means that the values are scaled to match a standard Gaussian distribution (mean=0, variance=1), across voxels, if there are now effects in reality. +# In order to get statistical significance, we form a t-statistic, and directly convert is into z-scale. The z-scale means that the values are scaled to match a standard Gaussian distribution (mean=0, variance=1), across voxels, if there were now effects in the data. z_map = fmri_glm.compute_contrast(active_minus_rest, output_type='z_score') ############################################################################### # Plot thresholded z scores map -# we display it on top of the average functional image of the series (could be the anatomical image of the subject). -# we use arbitrarily a threshold of 3.0 in z-scale. We'll see later how to use corrected thresholds. +# We display it on top of the average functional image of the series (could be the anatomical image of the subject). +# We use arbitrarily a threshold of 3.0 in z-scale. We'll see later how to use corrected thresholds. # we show to display 3 axial views: display_mode='z', cut_coords=3 plot_stat_map(z_map, bg_img=mean_img, threshold=3.0, @@ -198,7 +208,7 @@ ############################################################################### # Statistical signifiance testing -# One should worry about the statistical validity of the procedure: here we used an arbitrary threshold of 3.0 but the threshold should provide some guarantees on the risk of false detections (aka type-1 errors in statistics). One first suggestion is to control the false positive rate (fpr) at a certain level, e.g. 0.001: +# One should worry about the statistical validity of the procedure: here we used an arbitrary threshold of 3.0 but the threshold should provide some guarantees on the risk of false detections (aka type-1 errors in statistics). One first suggestion is to control the false positive rate (fpr) at a certain level, e.g. 0.001: this means that there is.1% chance of declaring active an inactive voxel. from nistats.thresholding import map_threshold _, threshold = map_threshold(z_map, threshold=.001, height_control='fpr') @@ -209,7 +219,7 @@ plt.show() ############################################################################### -# The problem is that with this you expect a fraction of 0.001 * n_voxels to show up while they're not active. A more conservative solution is to control the family wise errro rate, i.e. the probability of making ony one false detection, say at 5%. For that we use the so-called Bonferroni correction +# The problem is that with this you expect 0.001 * n_voxels to show up while they're not active --- tens to hundreds of voxels. A more conservative solution is to control the family wise errro rate, i.e. the probability of making ony one false detection, say at 5%. For that we use the so-called Bonferroni correction _, threshold = map_threshold(z_map, threshold=.05, height_control='bonferroni') print('Bonferroni-corrected, p<0.05 threshold: %.3f' % threshold) @@ -279,7 +289,6 @@ effects_of_interest = np.vstack((conditions['active'], conditions['rest'])) z_map = fmri_glm.compute_contrast(effects_of_interest, output_type='z_score') -from nistats.reporting import plot_contrast_matrix plot_contrast_matrix(effects_of_interest, design_matrix) plt.show() @@ -292,3 +301,6 @@ display_mode='z', cut_coords=3, black_bg=True, title='Effects of interest (fdr=0.05), clusters > 10 voxels') plt.show() + +############################################################################### +# Oops, there is a lot of non-neural signal in there (ventricles, arteries)... From 8abde5309d10cbf9cb18a8e8a887a1ca032d66d8 Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Mon, 1 Oct 2018 16:51:31 +0200 Subject: [PATCH 093/210] first-level localizer fMRI dataset events file is now BIDS compliant - Added _make_localizer_first_level_paradigm_file_bids_compliant() to overwrite the downloaded events file with BIDS compliant version. - Added unit test for above function in nistats/tests/ - nistats/datasets.py imports reordered in compliance with PEP8. --- nistats/datasets.py | 44 +++++++++++++--- ...irst_level_paradigm_file_bids_compliant.py | 52 +++++++++++++++++++ 2 files changed, 89 insertions(+), 7 deletions(-) create mode 100644 nistats/tests/test_make_localizer_first_level_paradigm_file_bids_compliant.py diff --git a/nistats/datasets.py b/nistats/datasets.py index 4767cb64..2936ee83 100644 --- a/nistats/datasets.py +++ b/nistats/datasets.py @@ -3,18 +3,22 @@ Author: Gael Varoquaux """ -import os import fnmatch -import re import glob import json +import os +import re + +from botocore.handlers import disable_signing import nibabel as nib +import pandas as pd +from nilearn.datasets.utils import (_fetch_file, + _fetch_files, + _get_dataset_dir, + _uncompress_file, + ) from sklearn.datasets.base import Bunch -from nilearn.datasets.utils import ( - _get_dataset_dir, _fetch_files, _fetch_file, _uncompress_file) - -from botocore.handlers import disable_signing SPM_AUDITORY_DATA_FILES = ["fM00223/fM00223_%03i.img" % index for index in range(4, 100)] @@ -273,6 +277,29 @@ def fetch_openneuro_dataset( return data_dir, sorted(downloaded) +def _make_localizer_first_level_paradigm_file_bids_compliant(paradigm_file): + """ Makes the first-level localizer fMRI dataset events file + BIDS compliant. Overwrites the original file. + Adds headers in first row. + Removes first column (spurious data). + Uses Tab character as value separator. + + Parameters + ---------- + paradigm_file: string + path to the localizer_first_level dataset's events file. + + Returns + ------- + None + """ + paradigm = pd.read_csv(paradigm_file, sep=' ', header=None, index_col=None, + names=['session', 'trial_type', 'onset'], + ) + paradigm.drop(labels='session', axis=1, inplace=True) + paradigm.to_csv(paradigm_file, sep='\t', index=False) + + def fetch_localizer_first_level(data_dir=None, verbose=1): """ Download a first-level localizer fMRI dataset @@ -303,7 +330,10 @@ def fetch_localizer_first_level(data_dir=None, verbose=1): verbose=verbose) params = dict(zip(sorted(files.keys()), sub_files)) - + _make_localizer_first_level_paradigm_file_bids_compliant(paradigm_file= + params['paradigm'] + ) + return Bunch(**params) diff --git a/nistats/tests/test_make_localizer_first_level_paradigm_file_bids_compliant.py b/nistats/tests/test_make_localizer_first_level_paradigm_file_bids_compliant.py new file mode 100644 index 00000000..2c868326 --- /dev/null +++ b/nistats/tests/test_make_localizer_first_level_paradigm_file_bids_compliant.py @@ -0,0 +1,52 @@ +import os +from tempfile import NamedTemporaryFile + +from nose.tools import assert_true +import pandas as pd +from nistats.datasets import _make_localizer_first_level_paradigm_file_bids_compliant + +def _input_data_for_test_file(): + file_data = [ + [0, 'calculvideo', 0.0], + [0, 'calculvideo', 2.400000095], + [0, 'damier_H', 8.699999809], + [0, 'clicDaudio', 11.39999961], + ] + return pd.DataFrame(file_data) + + +def _expected_output_data_from_test_file(): + file_data = [ + ['calculvideo', 0.0], + ['calculvideo', 2.400000095], + ['damier_H', 8.699999809], + ['clicDaudio', 11.39999961], + ] + file_data = pd.DataFrame(file_data) + file_data.columns = ['trial_type', 'onset'] + return file_data + + +def run_test(): + data_for_tests = _input_data_for_test_file() + expected_data_from_test_file = _expected_output_data_from_test_file() + with NamedTemporaryFile(mode='w', + dir=os.getcwd(), + suffix='.csv') as temp_csv_obj: + data_for_tests.to_csv(temp_csv_obj.name, + index=False, + header=False, + sep=' ', + ) + _make_localizer_first_level_paradigm_file_bids_compliant( + temp_csv_obj.name + ) + data_from_test_file_post_mod = pd.read_csv(temp_csv_obj.name, sep='\t') + assert_true(all( + expected_data_from_test_file == data_from_test_file_post_mod + ) + ) + + +if __name__ == '__main__': + run_test() From fdcc2183295aa3a9b292e963cfced1b6947c4e2c Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Mon, 1 Oct 2018 17:05:30 +0200 Subject: [PATCH 094/210] utils._verify_events_file_uses_tab_separators() no longer returns any value - Modified unit tests in tests/test_verify_events_file_uses_tab_separators accordingly. --- ..._verify_events_file_uses_tab_separators.py | 22 +++++-------------- nistats/utils.py | 22 +++++++------------ 2 files changed, 14 insertions(+), 30 deletions(-) diff --git a/nistats/tests/test_verify_events_file_uses_tab_separators.py b/nistats/tests/test_verify_events_file_uses_tab_separators.py index 147c9c44..1c7be885 100644 --- a/nistats/tests/test_verify_events_file_uses_tab_separators.py +++ b/nistats/tests/test_verify_events_file_uses_tab_separators.py @@ -42,7 +42,7 @@ def _run_test_for_invalid_separator(filepath, delimiter_name): _verify_events_file_uses_tab_separators(events_files=filepath) else: result = _verify_events_file_uses_tab_separators(events_files=filepath) - assert_true(result == []) + assert_true(result is None) def test_for_invalid_separator(): @@ -65,9 +65,7 @@ def test_with_2D_dataframe(): events_pandas_dataframe = pd.DataFrame(data_for_pandas_dataframe) result = _verify_events_file_uses_tab_separators( events_files=events_pandas_dataframe) - expected_error = result[0][1] - with assert_raises(TypeError): - raise expected_error + assert_true(result is None) def test_with_1D_dataframe(): @@ -76,27 +74,20 @@ def test_with_1D_dataframe(): events_pandas_dataframe = pd.DataFrame(dataframe_) result = _verify_events_file_uses_tab_separators( events_files=events_pandas_dataframe) - expected_error = result[0][1] - with assert_raises(TypeError): - raise expected_error - + assert_true(result is None) def test_for_invalid_filepath(): filepath = 'junk_file_path.csv' result = _verify_events_file_uses_tab_separators(events_files=filepath) - expected_error = result[0][1] - with assert_raises(IOError): - raise expected_error + assert_true(result is None) def test_for_pandas_dataframe(): events_pandas_dataframe = pd.DataFrame([['a', 'b', 'c'], [0, 1, 2]]) result = _verify_events_file_uses_tab_separators( events_files=events_pandas_dataframe) - expected_error = result[0][1] - with assert_raises(TypeError): - raise expected_error - + assert_true(result is None) + def test_binary_opening_an_image(): img_data = bytearray( @@ -143,5 +134,4 @@ def run_test_suite(): _run_tests_print_test_messages(test_func=test_) - run_test_suite() diff --git a/nistats/utils.py b/nistats/utils.py index 29a7feb9..a0d86b39 100644 --- a/nistats/utils.py +++ b/nistats/utils.py @@ -66,11 +66,7 @@ def _verify_events_file_uses_tab_separators(events_files): Returns ------- - errors_raised: List[List[(events filepath or dataframe, error]] - Possible errors: - TypeError: If events is a pandadas dataframe. - UnicodeDecodeError: If events is a binary file (Python3 only) - IOError: If events file's path is invalid + None Raises ------ @@ -79,17 +75,16 @@ def _verify_events_file_uses_tab_separators(events_files): """ valid_separators = [',', '\t'] events_files = [events_files] if not isinstance(events_files, (list, tuple)) else events_files - errors_raised = [] for events_file_ in events_files: try: with open(events_file_, 'r') as events_file_obj: events_file_sample = events_file_obj.readline() - except TypeError as type_err: - errors_raised.append([events_file_, type_err]) # events is Pandas dataframe. - except UnicodeDecodeError as unicode_err: - errors_raised.append([events_file_, unicode_err]) # py3:if binary file - except IOError as io_err: - errors_raised.append([events_file_, io_err]) # if invalid filepath. + except TypeError as type_err: # events is Pandas dataframe. + pass + except UnicodeDecodeError as unicode_err: # py3:if binary file + pass + except IOError as io_err: # if invalid filepath. + pass else: try: csv.Sniffer().sniff(sample=events_file_sample, @@ -100,8 +95,7 @@ def _verify_events_file_uses_tab_separators(events_files): 'The values in the events file are not separated by tabs; ' 'please enforce BIDS conventions', events_file_) - return errors_raised - + def _check_run_tables(run_imgs, tables_, tables_name): """Check fMRI runs and corresponding tables to raise error if necessary""" From 0bf3715a317f5a200990cdc37a206a9adedffcd8 Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Tue, 2 Oct 2018 15:07:51 +0200 Subject: [PATCH 095/210] Events file for localizer_first_level is no longer modified in/for examples --- examples/02_first_level_models/plot_localizer_analysis.py | 3 +-- .../02_first_level_models/plot_localizer_surface_analysis.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/examples/02_first_level_models/plot_localizer_analysis.py b/examples/02_first_level_models/plot_localizer_analysis.py index 88681f53..13228008 100644 --- a/examples/02_first_level_models/plot_localizer_analysis.py +++ b/examples/02_first_level_models/plot_localizer_analysis.py @@ -31,8 +31,7 @@ # Prepare data data = datasets.fetch_localizer_first_level() paradigm_file = data.paradigm -paradigm = pd.read_csv(paradigm_file, sep=' ', header=None, index_col=None) -paradigm.columns = ['session', 'trial_type', 'onset'] +paradigm = pd.read_csv(paradigm_file, sep='\t', index_col=None) fmri_img = data.epi_img ######################################################################### diff --git a/examples/02_first_level_models/plot_localizer_surface_analysis.py b/examples/02_first_level_models/plot_localizer_surface_analysis.py index f798d762..ba7fd046 100644 --- a/examples/02_first_level_models/plot_localizer_surface_analysis.py +++ b/examples/02_first_level_models/plot_localizer_surface_analysis.py @@ -29,8 +29,7 @@ from nistats.datasets import fetch_localizer_first_level data = fetch_localizer_first_level() paradigm_file = data.paradigm -paradigm = pd.read_csv(paradigm_file, sep=' ', header=None, index_col=None) -paradigm.columns = ['session', 'trial_type', 'onset'] +paradigm = pd.read_csv(paradigm_file, sep='\t', index_col=None) fmri_img = data.epi_img ######################################################################### From 24fd8e6864af4ba0d47ee6f4d64378e315a4c74d Mon Sep 17 00:00:00 2001 From: bthirion Date: Tue, 2 Oct 2018 15:10:55 +0200 Subject: [PATCH 096/210] A bunch of updates in the tutorial --- .../plot_first_level_model_details.py | 92 ++++++++++++++----- .../plot_single_subject_single_run.py | 17 ++-- 2 files changed, 76 insertions(+), 33 deletions(-) diff --git a/examples/01_tutorials/plot_first_level_model_details.py b/examples/01_tutorials/plot_first_level_model_details.py index fc479342..1d777fc0 100644 --- a/examples/01_tutorials/plot_first_level_model_details.py +++ b/examples/01_tutorials/plot_first_level_model_details.py @@ -10,8 +10,8 @@ resulting brain maps. Readers without prior experience in fMRI data analysis should first -run the plot_sing_subject_single_run tutorial to get a bit more -familiar with the base concepts, and only then run thi script. +run the :ref:`plot_single_subject_single_run` tutorial to get a bit more +familiar with the base concepts, and only then run this tutorial example. To run this example, you must launch IPython via ``ipython --matplotlib`` in a terminal, or use ``jupyter-notebook``. @@ -25,7 +25,6 @@ import numpy as np import pandas as pd from nilearn import plotting - from nistats import datasets ############################################################################### @@ -36,37 +35,51 @@ # acquisition of a fast event-related dataset. data = datasets.fetch_localizer_first_level() +fmri_img = data.epi_img + +############################################################################### +# Define the paradigm that will be used +# +# We just get the provided file and make it BIDS-compliant. t_r = 2.4 paradigm_file = data.paradigm events= pd.read_csv(paradigm_file, sep=' ', header=None, index_col=None) events.columns = ['session', 'trial_type', 'onset'] -fmri_img = data.epi_img ############################################################################### # Running a basic model # --------------------- - +# +# First specify a linear model. +# the fit() model creates the design matrix and the beta maps. +# from nistats.first_level_model import FirstLevelModel first_level_model = FirstLevelModel(t_r) first_level_model = first_level_model.fit(fmri_img, events=events) design_matrix = first_level_model.design_matrices_[0] +######################################################################### +# Let us take a look at the design matrix: it has 10 main columns corresponding to 10 experimental conditions, followed by 3 columns describing low-frequency signals (drifts) and a constant regressor. from nistats.reporting import plot_design_matrix plot_design_matrix(design_matrix) +import matplotlib.pyplot as plt +plt.show() ######################################################################### -# Specify the contrasts. +# Specification of the contrasts. # -# For this, let's create a function that, given the deisgn matrix, +# For this, let's create a function that, given the design matrix, # generates the corresponding contrasts. -# This will be useful +# This will be useful to repeat contast specification when we change the design matrix. def make_localizer_contrasts(design_matrix): """ returns a dictionary of four contasts, given the design matrix""" + # first generate canonical contrasts contrast_matrix = np.eye(design_matrix.shape[1]) contrasts = dict([(column, contrast_matrix[i]) for i, column in enumerate(design_matrix.columns)]) + # Add more complex contrasts contrasts["audio"] = contrasts["clicDaudio"] + contrasts["clicGaudio"] +\ contrasts["calculaudio"] + contrasts["phraseaudio"] @@ -75,8 +88,7 @@ def make_localizer_contrasts(design_matrix): contrasts["computation"] = contrasts["calculaudio"] + contrasts["calculvideo"] contrasts["sentences"] = contrasts["phraseaudio"] + contrasts["phrasevideo"] - ######################################################################### - # Short list of more relevant contrasts + # Short dictionary of more relevant contrasts contrasts = { "left-right": (contrasts["clicGaudio"] + contrasts["clicGvideo"] - contrasts["clicDaudio"] - contrasts["clicDvideo"]), @@ -87,23 +99,32 @@ def make_localizer_contrasts(design_matrix): } return contrasts +######################################################################### +# So let's look at these computed contrasts + contrasts = make_localizer_contrasts(design_matrix) -# TODO: plot contrasts +plt.figure(figsize=(5, 9)) +from nistats.reporting import plot_contrast_matrix +for i, (key, values) in enumerate(contrasts.items()): + ax = plt.subplot(5, 1, i + 1) + plot_contrast_matrix(values, design_matrix=design_matrix, ax=ax) + +plt.show() ######################################################################### -# contrast estimation and plotting +# Contrast estimation and plotting +# # Since this script will be repeated several times, for the sake of readbility, # we encapsulate it in a function that we call when needed. # -import matplotlib.pyplot as plt - def plot_contrast(first_level_model): """ Given a first model, specify, enstimate and plot the main contrasts""" design_matrix = first_level_model.design_matrices_[0] # Call the contrast specification within the function contrasts = make_localizer_contrasts(design_matrix) fig = plt.figure(figsize=(11, 3)) + # compute the per-contrast z-map for index, (contrast_id, contrast_val) in enumerate(contrasts.items()): ax = plt.subplot(1, len(contrasts), 1 + index) z_map = first_level_model.compute_contrast( @@ -112,6 +133,10 @@ def plot_contrast(first_level_model): z_map, display_mode='z', threshold=3.0, title=contrast_id, axes=ax, cut_coords=1) +######################################################################### +# Let's run the model and look at the outcome. + +plot_contrast(first_level_model) plt.show() ######################################################################### @@ -134,9 +159,9 @@ def plot_contrast(first_level_model): plt.show() ######################################################################### -# Note that the design matrix has more columns to model dirft terms +# Note that the design matrix has more columns to model drifts in the data. # -# Anyway, this model performs rather poorly +# We notice however that this model performs rather poorly. # # Another solution is to remove these drift terms. Maybe they're simply useless. # this is done by setting drift_model to None. @@ -172,7 +197,7 @@ def plot_contrast(first_level_model): # # This is the filter used to convert the event sequence into a reference BOLD signal for the design matrix. # -# The first thing that we can do is to change the default model (the so-called Glover hrf) for the so-called canocial model of SPM --which has slightly weaker undershoot component. +# The first thing that we can do is to change the default model (the so-called Glover hrf) for the so-called canonical model of SPM --which has slightly weaker undershoot component. first_level_model = FirstLevelModel(t_r, hrf_model='spm') first_level_model = first_level_model.fit(fmri_img, events=events) @@ -184,7 +209,7 @@ def plot_contrast(first_level_model): ######################################################################### # No strong --positive or negative-- effect. # -# We could try to go one step further: using not only the so-called canocial hrf, but also its time derivative. Note that in that case, we still perform the contrasts and obtain statistical significance for the main effect ---not the time derivative. This means that the inclusion of time derivative in the design matrix has the sole effect of discounting timing misspecification from the error term, which vould decrease the estimated variance and enhance the statistical significance of the effect. Is it the case ? +# We could try to go one step further: using not only the so-called canonical hrf, but also its time derivative. Note that in that case, we still perform the contrasts and obtain statistical significance for the main effect ---not the time derivative. This means that the inclusion of time derivative in the design matrix has the sole effect of discounting timing misspecification from the error term, which vould decrease the estimated variance and enhance the statistical significance of the effect. Is it the case ? first_level_model = FirstLevelModel(t_r, hrf_model='spm + derivative') first_level_model = first_level_model.fit(fmri_img, events=events) @@ -194,7 +219,7 @@ def plot_contrast(first_level_model): plt.show() ######################################################################### -# Not a huge effect, but rather positive overall. We could keep that one +# Not a huge effect, but rather positive overall. We could keep that one. # # Bzw, a benefit of this approach is that we can test which voxels are well explined by the derivative term, hinting at misfit regions, a possibly valuable information # This is implemented by an F test on the time derivative regressors. @@ -209,11 +234,25 @@ def plot_contrast(first_level_model): ######################################################################### # We don't see too much here: the onset times and hrf delay we're using are probably fine. +######################################################################### +# We can also consider adding the so-called dispersion derivative to capture some mis-specification in the shape of the hrf. +# this is done by specifying `hrf_model='spm + derivative + dispersion'` +# +first_level_model = FirstLevelModel(t_r, hrf_model='spm + derivative + dispersion') +first_level_model = first_level_model.fit(fmri_img, events=events) +design_matrix = first_level_model.design_matrices_[0] +plot_design_matrix(design_matrix) +plot_contrast(first_level_model) +plt.show() + +######################################################################### +# Not a huge effect. For the sake of simplicity and readibility, we can drop that one. + ######################################################################### # The noise model ar(1) or ols ? # ------------------------------ # -# So far,we have implitly use an lag-1 autoregressive model ---aka ar(1)--- for the temporal structure of the noise. an alternative choice is to use an ordinaly least sqaure model (ols) that neglects that assumes no temporal structure (independent noise) +# So far,we have implicitly used a lag-1 autoregressive model ---aka ar(1)--- for the temporal structure of the noise. An alternative choice is to use an ordinaly least squares model (ols) that assumes no temporal structure (time-independent noise) first_level_model = FirstLevelModel(t_r, hrf_model='spm + derivative', noise_model='ols') first_level_model = first_level_model.fit(fmri_img, events=events) @@ -229,9 +268,12 @@ def plot_contrast(first_level_model): # Removing confounds # ------------------ # -# A problematic feature of fMRI is the presence of unconctrolled confounds in the data, sue to scanner instabilities (spikes) or physiological phenomena (motion, heart and repoiration rate) -# Side measurements are sometimes acquired to charcterise these effects. We don't have access to those. -# What we can do instead id to estimate confounding effects from the data themselves, using the compcorr approach, and take those nto account in the model +# A problematic feature of fMRI is the presence of unconctrolled confounds in the data, sue to scanner instabilities (spikes) or physiological phenomena, such as motion, heart and respiration-related blood oxygenation flucturations. +# Side measurements are sometimes acquired to charcterise these effects. Here we don't have access to those. +# What we can do instead is to estimate confounding effects from the data themselves, using the compcorr approach, and take those into account in the model. +# +# For this we rely on the so-called :ref:`high_variance_confounds ` routine of Nilearn. + from nilearn.image import high_variance_confounds confounds = pd.DataFrame(high_variance_confounds(fmri_img, percentile=1)) @@ -246,7 +288,7 @@ def plot_contrast(first_level_model): ######################################################################### # Note the five additional columns in the design matrix # -# The effect on activation maps is complex: auditory/visual effects are killed, probably because they were somewhat colinear to the confounds. On the other hand, some of the maps become cleaner (H-V, computation) after this effects. +# The effect on activation maps is complex: auditory/visual effects are killed, probably because they were somewhat colinear to the confounds. On the other hand, some of the maps become cleaner (H-V, computation) after this addition. ######################################################################### @@ -275,7 +317,7 @@ def plot_contrast(first_level_model): # Masking consists in selecting the region of the image on which the model is run: it is useless to run it outside of the brain. # the approach taken by FirstLeveModel is to estimate it from the fMRI data themselves when no mask is explicitly provided. # Since the data have been resampled into MNI space, we can use instead a mask of the grey matter in MNI space. The benefit is that it makes voxel-level comparisons easier across subjects and datasets, and removed non-grey matter regions, in which no BOLD signal is expected. -# The down side is that the mask may not fit very well these particular data +# The downside is that the mask may not fit very well these particular data. from nilearn.plotting import plot_roi from nilearn.datasets import fetch_icbm152_brain_gm_mask diff --git a/examples/01_tutorials/plot_single_subject_single_run.py b/examples/01_tutorials/plot_single_subject_single_run.py index e1386476..de6e4b42 100644 --- a/examples/01_tutorials/plot_single_subject_single_run.py +++ b/examples/01_tutorials/plot_single_subject_single_run.py @@ -133,14 +133,12 @@ ############################################################################### # One can inspect the design matrix (rows represent time, and -# columns contain the predictors): - +# columns contain the predictors). design_matrix = fmri_glm.design_matrices_[0] ############################################################################### # Formally, we have taken the first design matrix, because the model is # implictily meant to for multiple runs. - from nistats.reporting import plot_design_matrix plot_design_matrix(design_matrix) import matplotlib.pyplot as plt @@ -177,10 +175,9 @@ active_minus_rest = conditions['active'] - conditions['rest'] ############################################################################### -# Let's look at it +# Let's look at it: plot the coefficients of the contrast, indexed by the names of the columns of the design matrix. from nistats.reporting import plot_contrast_matrix - plot_contrast_matrix(active_minus_rest, design_matrix=design_matrix) ############################################################################### @@ -283,13 +280,17 @@ # Performing an F-test # # "active vs rest" is a typical t test: condition versus baseline. Another popular type of test is an F test in which one seeks whether a certain combination of conditions (possibly two-, three- or higher-dimensional) explains a significant proportion of the signal. -# Here one might for instance test which voxels are well explained by combination of the active and rest condition. Atcually, the contrast specification is done exactly the same way as for t contrasts. - +# Here one might for instance test which voxels are well explained by combination of the active and rest condition. import numpy as np effects_of_interest = np.vstack((conditions['active'], conditions['rest'])) +plot_contrast_matrix(effects_of_interest, design_matrix) +plt.show() + +############################################################################### +# Specify the contrast and compute the correspoding map. Actually, the contrast specification is done exactly the same way as for t contrasts. + z_map = fmri_glm.compute_contrast(effects_of_interest, output_type='z_score') -plot_contrast_matrix(effects_of_interest, design_matrix) plt.show() ############################################################################### From b5a0293973569f16eb60c0dfe0e9f419938f09a2 Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Tue, 2 Oct 2018 15:41:18 +0200 Subject: [PATCH 097/210] localizer_first_level events file not modified if already BIDS compliant --- nistats/datasets.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/nistats/datasets.py b/nistats/datasets.py index 2936ee83..57c1aca3 100644 --- a/nistats/datasets.py +++ b/nistats/datasets.py @@ -277,6 +277,11 @@ def fetch_openneuro_dataset( return data_dir, sorted(downloaded) +def _check_bids_compliance_localizer_first_level_paradigm_file(paradigm_file): + paradigm = pd.read_csv(paradigm_file, sep='\t') + return list(paradigm.columns) == ['trial_type', 'onset'] + + def _make_localizer_first_level_paradigm_file_bids_compliant(paradigm_file): """ Makes the first-level localizer fMRI dataset events file BIDS compliant. Overwrites the original file. @@ -330,7 +335,13 @@ def fetch_localizer_first_level(data_dir=None, verbose=1): verbose=verbose) params = dict(zip(sorted(files.keys()), sub_files)) - _make_localizer_first_level_paradigm_file_bids_compliant(paradigm_file= + bids_compliant_paradigm = ( + _check_bids_compliance_localizer_first_level_paradigm_file( + paradigm_file=params['paradigm'] + ) + ) + if not bids_compliant_paradigm: + _make_localizer_first_level_paradigm_file_bids_compliant(paradigm_file= params['paradigm'] ) From 69c78ce1be5570daa8b84e8f94295e4cad834fab Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Tue, 2 Oct 2018 15:44:27 +0200 Subject: [PATCH 098/210] Renamed _make_localizer_first_level_paradigm_file_bids_compliant() - Renamed _make_localizer_first_level_paradigm_file_bids_compliant() to _make_bids_compliant_localizer_first_level_paradigm_file() --- nistats/datasets.py | 6 +++--- ...ke_localizer_first_level_paradigm_file_bids_compliant.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/nistats/datasets.py b/nistats/datasets.py index 57c1aca3..d93d8327 100644 --- a/nistats/datasets.py +++ b/nistats/datasets.py @@ -282,7 +282,7 @@ def _check_bids_compliance_localizer_first_level_paradigm_file(paradigm_file): return list(paradigm.columns) == ['trial_type', 'onset'] -def _make_localizer_first_level_paradigm_file_bids_compliant(paradigm_file): +def _make_bids_compliant_localizer_first_level_paradigm_file(paradigm_file): """ Makes the first-level localizer fMRI dataset events file BIDS compliant. Overwrites the original file. Adds headers in first row. @@ -341,9 +341,9 @@ def fetch_localizer_first_level(data_dir=None, verbose=1): ) ) if not bids_compliant_paradigm: - _make_localizer_first_level_paradigm_file_bids_compliant(paradigm_file= + _make_bids_compliant_localizer_first_level_paradigm_file(paradigm_file= params['paradigm'] - ) + ) return Bunch(**params) diff --git a/nistats/tests/test_make_localizer_first_level_paradigm_file_bids_compliant.py b/nistats/tests/test_make_localizer_first_level_paradigm_file_bids_compliant.py index 2c868326..6bb21b3d 100644 --- a/nistats/tests/test_make_localizer_first_level_paradigm_file_bids_compliant.py +++ b/nistats/tests/test_make_localizer_first_level_paradigm_file_bids_compliant.py @@ -3,7 +3,7 @@ from nose.tools import assert_true import pandas as pd -from nistats.datasets import _make_localizer_first_level_paradigm_file_bids_compliant +from nistats.datasets import _make_bids_compliant_localizer_first_level_paradigm_file def _input_data_for_test_file(): file_data = [ @@ -38,7 +38,7 @@ def run_test(): header=False, sep=' ', ) - _make_localizer_first_level_paradigm_file_bids_compliant( + _make_bids_compliant_localizer_first_level_paradigm_file( temp_csv_obj.name ) data_from_test_file_post_mod = pd.read_csv(temp_csv_obj.name, sep='\t') From 749af04e7376be1d2d5cf3d260c46c67caa03840 Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Tue, 2 Oct 2018 15:46:38 +0200 Subject: [PATCH 099/210] Renamed tests/test_make_localizer_first_level_paradigm_file_bids_compliant.py - Renamed test_make_localizer_first_level_paradigm_file_bids_compliant.py to test_make_bids_compliant_localizer_first_level_paradigm_file.py --- ...st_make_bids_compliant_localizer_first_level_paradigm_file.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename nistats/tests/{test_make_localizer_first_level_paradigm_file_bids_compliant.py => test_make_bids_compliant_localizer_first_level_paradigm_file.py} (100%) diff --git a/nistats/tests/test_make_localizer_first_level_paradigm_file_bids_compliant.py b/nistats/tests/test_make_bids_compliant_localizer_first_level_paradigm_file.py similarity index 100% rename from nistats/tests/test_make_localizer_first_level_paradigm_file_bids_compliant.py rename to nistats/tests/test_make_bids_compliant_localizer_first_level_paradigm_file.py From 00a3a6b91d9a903fa60662dc6d60c269c95d2ab9 Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Tue, 2 Oct 2018 17:23:06 +0200 Subject: [PATCH 100/210] BIDS compliance checked in fetcher by _verify_events_file_uses_tab_separators() - Pre-mod BIDS compliance check is done by utils._verify_events_file_uses_tab_separators() TravisCI failure, possibly due to previously used function. - Removed datasets._check_bids_compliance_localizer_first_level_paradigm_file() --- nistats/datasets.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/nistats/datasets.py b/nistats/datasets.py index d93d8327..448814af 100644 --- a/nistats/datasets.py +++ b/nistats/datasets.py @@ -19,6 +19,8 @@ ) from sklearn.datasets.base import Bunch +from nistats.utils import _verify_events_file_uses_tab_separators + SPM_AUDITORY_DATA_FILES = ["fM00223/fM00223_%03i.img" % index for index in range(4, 100)] @@ -277,11 +279,6 @@ def fetch_openneuro_dataset( return data_dir, sorted(downloaded) -def _check_bids_compliance_localizer_first_level_paradigm_file(paradigm_file): - paradigm = pd.read_csv(paradigm_file, sep='\t') - return list(paradigm.columns) == ['trial_type', 'onset'] - - def _make_bids_compliant_localizer_first_level_paradigm_file(paradigm_file): """ Makes the first-level localizer fMRI dataset events file BIDS compliant. Overwrites the original file. @@ -335,12 +332,9 @@ def fetch_localizer_first_level(data_dir=None, verbose=1): verbose=verbose) params = dict(zip(sorted(files.keys()), sub_files)) - bids_compliant_paradigm = ( - _check_bids_compliance_localizer_first_level_paradigm_file( - paradigm_file=params['paradigm'] - ) - ) - if not bids_compliant_paradigm: + try: + _verify_events_file_uses_tab_separators(params['paradigm']) + except ValueError: _make_bids_compliant_localizer_first_level_paradigm_file(paradigm_file= params['paradigm'] ) From 4b9f3a760a40cc266990d90bbc6e023d4fe9fbfd Mon Sep 17 00:00:00 2001 From: bthirion Date: Tue, 2 Oct 2018 20:03:58 +0200 Subject: [PATCH 101/210] slightly improving readability of the example --- examples/01_tutorials/plot_bids_analysis.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/examples/01_tutorials/plot_bids_analysis.py b/examples/01_tutorials/plot_bids_analysis.py index 96ffe519..80c86a97 100644 --- a/examples/01_tutorials/plot_bids_analysis.py +++ b/examples/01_tutorials/plot_bids_analysis.py @@ -84,21 +84,30 @@ # contrast that reveals the language network (language - string). Notice that # we can define a contrast using the names of the conditions especified in the # events dataframe. Sum, substraction and scalar multiplication are allowed. + +############################################################################ +# set the threshold as the z-variate with an uncorrected p-value of 0.001 +from scipy.stats import norm +p001_unc = norm.isf(0.001) + +############################################################################ +# Prepare figure for concurrent plot of individual maps from nilearn import plotting import matplotlib.pyplot as plt -from scipy.stats import norm -fig, axes = plt.subplots(nrows=2, ncols=5) +fig, axes = plt.subplots(nrows=2, ncols=5, figsize=(8, 4.5)) model_and_args = zip(models, models_run_imgs, models_events, models_confounds) for midx, (model, imgs, events, confounds) in enumerate(model_and_args): + # fit the GLM model.fit(imgs, events, confounds) + # compute the contrast of interest zmap = model.compute_contrast('language-string') - plotting.plot_glass_brain(zmap, colorbar=False, threshold=norm.isf(0.001), + plotting.plot_glass_brain(zmap, colorbar=False, threshold=p001_unc, title=('sub-' + model.subject_label), axes=axes[int(midx / 5), int(midx % 5)], plot_abs=False, display_mode='x') fig.suptitle('subjects z_map language network (unc p<0.001)') -plt.show() +plotting.show() ######################################################################### # Second level model estimation @@ -122,7 +131,7 @@ ######################################################################### # The group level contrast reveals a left lateralized fronto-temporal # language network -plotting.plot_glass_brain(zmap, colorbar=True, threshold=norm.isf(0.001), +plotting.plot_glass_brain(zmap, colorbar=True, threshold=p001_unc, title='Group language network (unc p<0.001)', plot_abs=False, display_mode='x') plotting.show() From 80f19b11eef7685527998b4aab467f1a823788c0 Mon Sep 17 00:00:00 2001 From: bthirion Date: Tue, 2 Oct 2018 20:04:23 +0200 Subject: [PATCH 102/210] moving plot_bids_feature to another directory --- .../{01_tutorials => 02_first_level_models}/plot_bids_features.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename examples/{01_tutorials => 02_first_level_models}/plot_bids_features.py (100%) diff --git a/examples/01_tutorials/plot_bids_features.py b/examples/02_first_level_models/plot_bids_features.py similarity index 100% rename from examples/01_tutorials/plot_bids_features.py rename to examples/02_first_level_models/plot_bids_features.py From de81a33743c6e085c711ed5dad4f4d47a2d266b6 Mon Sep 17 00:00:00 2001 From: bthirion Date: Tue, 2 Oct 2018 22:42:20 +0200 Subject: [PATCH 103/210] Cleaned examples --- .../plot_fiac_analysis.py | 71 +++++--- .../plot_localizer_surface_analysis.py | 155 +++++++++++++----- 2 files changed, 163 insertions(+), 63 deletions(-) diff --git a/examples/02_first_level_models/plot_fiac_analysis.py b/examples/02_first_level_models/plot_fiac_analysis.py index fa0a4fdd..29f77d51 100644 --- a/examples/02_first_level_models/plot_fiac_analysis.py +++ b/examples/02_first_level_models/plot_fiac_analysis.py @@ -1,9 +1,9 @@ -""" -Simple example of GLM fitting in fMRI -====================================== +"""Simple example of two-session fMRI model fitting +================================================ Full step-by-step example of fitting a GLM to experimental data and visualizing the results. This is done on two runs of one subject of the FIAC dataset. + For details on the data, please see: Dehaene-Lambertz G, Dehaene S, Anton JL, Campagne A, Ciuciu P, Dehaene @@ -20,20 +20,17 @@ 4. A GLM is applied to the dataset (effect/covariance, then contrast estimation) -""" -from os import mkdir, path, getcwd - -import numpy as np -import pandas as pd - -from nilearn import plotting -from nilearn.image import mean_img +Technically, this example shows how to handle two sessions that +contain the same experimental conditions. The model directly returns a +fixed effect of the statistics across the two sessions. -from nistats.first_level_model import FirstLevelModel -from nistats import datasets +""" -# write directory +############################################################################### +# Create a write directory to work +# it will be a 'results' subdirectory of the current directory. +from os import mkdir, path, getcwd write_dir = path.join(getcwd(), 'results') if not path.exists(write_dir): mkdir(write_dir) @@ -41,16 +38,31 @@ ######################################################################### # Prepare data and analysis parameters # -------------------------------------- +# +# Note that there are two sessions + +from nistats import datasets data = datasets.fetch_fiac_first_level() fmri_img = [data['func1'], data['func2']] + +######################################################################### +# Create a mean image for plotting puepose +from nilearn.image import mean_img mean_img_ = mean_img(fmri_img[0]) + +######################################################################### +# The design matrices were pre-computed, we simply put them in a list of DataFrames design_files = [data['design_matrix1'], data['design_matrix2']] +import pandas as pd +import numpy as np design_matrices = [pd.DataFrame(np.load(df)['X']) for df in design_files] ######################################################################### # GLM estimation # ---------------------------------- # GLM specification + +from nistats.first_level_model import FirstLevelModel fmri_glm = FirstLevelModel(mask=data['mask'], minimize_memory=True) ######################################################################### @@ -58,13 +70,16 @@ fmri_glm = fmri_glm.fit(fmri_img, design_matrices=design_matrices) ######################################################################### -# compute fixed effects of the two runs and compute related images +# Compute fixed effects of the two runs and compute related images +# For this, we first define the contrasts as we would do for a single session n_columns = design_matrices[0].shape[1] - def pad_vector(contrast_, n_columns): + """A small routine to append zeros in contrast vectors""" return np.hstack((contrast_, np.zeros(n_columns - len(contrast_)))) +######################################################################### +# Contrast specification contrasts = {'SStSSp_minus_DStDSp': pad_vector([1, 0, 0, -1], n_columns), 'DStDSp_minus_SStSSp': pad_vector([-1, 0, 0, 1], n_columns), @@ -75,20 +90,32 @@ def pad_vector(contrast_, n_columns): 'Deactivation': pad_vector([-1, -1, -1, -1, 4], n_columns), 'Effects_of_interest': np.eye(n_columns)[:5]} +######################################################################### +# Compute and plot statistics + +from nilearn import plotting print('Computing contrasts...') for index, (contrast_id, contrast_val) in enumerate(contrasts.items()): print(' Contrast % 2i out of %i: %s' % ( index + 1, len(contrasts), contrast_id)) - z_image_path = path.join(write_dir, '%s_z_map.nii' % contrast_id) + # estimate the contasts + # note that the model implictly compute a fixed effects across the two sessions z_map = fmri_glm.compute_contrast( contrast_val, output_type='z_score') + + # Write the resulting stat images to file + z_image_path = path.join(write_dir, '%s_z_map.nii' % contrast_id) z_map.to_filename(z_image_path) - # make a snapshot of the contrast activation - if contrast_id == 'Effects_of_interest': - display = plotting.plot_stat_map( - z_map, bg_img=mean_img_, threshold=2.5, title=contrast_id) - display.savefig(path.join(write_dir, '%s_z_map.png' % contrast_id)) +######################################################################### +# make a snapshot of the 'Effects_of_interest' contrast map +zmap = path.join(write_dir, 'Effects_of_interest_z_map.nii') +display = plotting.plot_stat_map( + zmap, bg_img=mean_img_, threshold=2.5, title=contrast_id) + +######################################################################### +# We can save the figure a posteriori +display.savefig(path.join(write_dir, '%s_z_map.png' % contrast_id)) print('All the results were witten in %s' % write_dir) plotting.show() diff --git a/examples/02_first_level_models/plot_localizer_surface_analysis.py b/examples/02_first_level_models/plot_localizer_surface_analysis.py index f798d762..53776f54 100644 --- a/examples/02_first_level_models/plot_localizer_surface_analysis.py +++ b/examples/02_first_level_models/plot_localizer_surface_analysis.py @@ -1,86 +1,143 @@ """ -First level analysis of localizer dataset -========================================= +Example of surface-based first-level analysis +============================================= -Full step-by-step example of fitting a GLM to experimental data and visualizing -the results. +Full step-by-step example of fitting a GLM to experimental data sampled on the cortical surface and visualizing the results. More specifically: 1. A sequence of fMRI volumes are loaded -2. A design matrix describing all the effects related to the data is computed -3. a mask of the useful brain volume is computed +2. fMRI data are projected onto a reference cortical surface (the freesurfer template, fsaverage) +3. A design matrix describing all the effects related to the data is computed 4. A GLM is applied to the dataset (effect/covariance, then contrast estimation) +The result of the analysis are statistical maps that are defined on the brain mesh. We disply them using Nilearn capabilities. + +The projection of fMRI data onto a given brain mesh requires that both are initially defined in the same space. + +* The functional data should be coregistered to the anatomy from which the mesh was obtained. + +* Another possibility, used here, is to project the normalized fMRI data to an MNI-coregistered mesh, such as fsaverage. + +The advantage of this second approach is that it makes it easy to run second-level analyses on the surface. On the other hand, it is obviously less accurate than using a subject-tailored mesh. + + """ -import numpy as np -import pandas as pd -import nilearn ######################################################################### # Prepare data and analysis parameters # ------------------------------------- -# Prepare timing +# Prepare timing parameters t_r = 2.4 slice_time_ref = 0.5 +######################################################################### # Prepare data +# First the fMRI data from nistats.datasets import fetch_localizer_first_level data = fetch_localizer_first_level() +fmri_img = data.epi_img + +######################################################################### +# Second the experimental paradigm paradigm_file = data.paradigm +import pandas as pd paradigm = pd.read_csv(paradigm_file, sep=' ', header=None, index_col=None) paradigm.columns = ['session', 'trial_type', 'onset'] -fmri_img = data.epi_img + ######################################################################### # Project the fMRI image to the surface # ------------------------------------- +# +# For this we need to get a mesh representing the geometry of the surface. +# we could use an individual mesh, but we first resort to a standard mesh, the so-called fsaverage5 template from the Freesurfer software. +import nilearn fsaverage = nilearn.datasets.fetch_surf_fsaverage5() + +######################################################################### +# The projection function simply takes the fMRI data and the mesh. +# Note that those correspond spatially, are they are bothin MNI space. from nilearn import surface texture = surface.vol_to_surf(fmri_img, fsaverage.pial_right) ######################################################################### # Perform first level analysis # ---------------------------- -# Create design matrix +# +# This involves computing the design matrix and fitting the model. +# We start by specifying the timing of fMRI frames + +import numpy as np +n_scans = texture.shape[1] +frame_times = t_r * (np.arange(n_scans) + .5) + +######################################################################### +# Create the design matrix +# +# We specify an hrf model containing Glover model and its time derivative +# the drift model is implicitly a cosine basis with period cutodd 128s. from nistats.design_matrix import make_design_matrix -frame_times = t_r * (np.arange(texture.shape[1]) + .5) -dmtx = make_design_matrix( +design_matrix = make_design_matrix( frame_times, paradigm=paradigm, hrf_model='glover + derivative') -# Setup and fit GLM +######################################################################### +# Setup and fit GLM. +# Note that the output consists in 2 variables: `labels` and `fit` +# `labels` tags voxels according to noise autocorrelation. +# `estimates` contains the parameter estimates. +# We keep them for later contrast computation. + from nistats.first_level_model import run_glm -labels, res = run_glm(texture.T, dmtx.values) +labels, estimates = run_glm(texture.T, design_matrix.values) ######################################################################### # Estimate contrasts # ------------------ # Specify the contrasts -contrast_matrix = np.eye(dmtx.shape[1]) +# For practical purpose, we first generate an identity matrix whose size is +# the number of columns of the design matrix +contrast_matrix = np.eye(design_matrix.shape[1]) -# first create elementary contrasts -contrasts = dict([(column, contrast_matrix[i]) - for i, column in enumerate(dmtx.columns)]) +######################################################################### +# first create basic contrasts +basic_contrasts = dict([(column, contrast_matrix[i]) + for i, column in enumerate(design_matrix.columns)]) -# create some intermediate contrasts -contrasts["audio"] = contrasts["clicDaudio"] + contrasts["clicGaudio"] +\ - contrasts["calculaudio"] + contrasts["phraseaudio"] -contrasts["video"] = contrasts["clicDvideo"] + contrasts["clicGvideo"] + \ - contrasts["calculvideo"] + contrasts["phrasevideo"] -contrasts["computation"] = contrasts["calculaudio"] + contrasts["calculvideo"] -contrasts["sentences"] = contrasts["phraseaudio"] + contrasts["phrasevideo"] +######################################################################### +# add some intermediate contrasts +basic_contrasts["audio"] = basic_contrasts["clicDaudio"] +\ + basic_contrasts["clicGaudio"] +\ + basic_contrasts["calculaudio"] +\ + basic_contrasts["phraseaudio"] +basic_contrasts["video"] = basic_contrasts["clicDvideo"] +\ + basic_contrasts["clicGvideo"] + \ + basic_contrasts["calculvideo"] +\ + basic_contrasts["phrasevideo"] +basic_contrasts["computation"] = basic_contrasts["calculaudio"] +\ + basic_contrasts["calculvideo"] +basic_contrasts["sentences"] = basic_contrasts["phraseaudio"] +\ + basic_contrasts["phrasevideo"] ######################################################################### -# Short list of more relevant contrasts +# Finally make a dictionary of more relevant contrasts +# +# * "left - right button press" probes motor activity in left versus right button presses +# * "audio - video" probes the difference of activity between listening to some content or reading the same type of content (instructions, stories) +# * "computation - sentences" looks at the activity when performing a mental comptation task versus simply reading sentences. +# +# Of course, we could define other contrasts, but we keep only 3 for simplicity. + contrasts = { - "left - right button press": ( - contrasts["clicGaudio"] + contrasts["clicGvideo"] - - contrasts["clicDaudio"] - contrasts["clicDvideo"]), - "audio - video": contrasts["audio"] - contrasts["video"], - "computation - sentences": (contrasts["computation"] - - contrasts["sentences"]) + "left - right button press": (basic_contrasts["clicGaudio"] + + basic_contrasts["clicGvideo"] - + basic_contrasts["clicDaudio"] - + basic_contrasts["clicDvideo"]), + "audio - video": basic_contrasts["audio"] - basic_contrasts["video"], + "computation - sentences": (basic_contrasts["computation"] - + basic_contrasts["sentences"]) } ######################################################################### @@ -88,13 +145,18 @@ from nistats.contrasts import compute_contrast from nilearn import plotting +######################################################################### +# iterate over contrasts for index, (contrast_id, contrast_val) in enumerate(contrasts.items()): print(' Contrast % i out of %i: %s, right hemisphere' % (index + 1, len(contrasts), contrast_id)) - # compute contrasts - contrast = compute_contrast(labels, res, contrast_val, contrast_type='t') + # compute contrast-related statistics + contrast = compute_contrast(labels, estimates, contrast_val, contrast_type='t') + # we present the Z-transform of the t map z_score = contrast.z_score() - + # we plot it on the surface, on the inflated fsaverage mesh, + # together with a suitable background to give an impression + # of the cortex folding. plotting.plot_surf_stat_map( fsaverage.infl_right, z_score, hemi='right', title=contrast_id, colorbar=True, @@ -102,16 +164,27 @@ ######################################################################### # Analysing the left hemisphere -# Note that it requires little additional code +# ----------------------------- +# +# Note that it requires little additional code! + +######################################################################### +# Project the fMRI data to the mesh texture = surface.vol_to_surf(fmri_img, fsaverage.pial_left) -labels, res = run_glm(texture.T, dmtx.values) + +######################################################################### +# Estimate the General Linear Model +labels, estimates = run_glm(texture.T, design_matrix.values) + +######################################################################### +# Create contrast-specific maps for index, (contrast_id, contrast_val) in enumerate(contrasts.items()): print(' Contrast % i out of %i: %s, left hemisphere' % (index + 1, len(contrasts), contrast_id)) # compute contrasts - contrast = compute_contrast(labels, res, contrast_val, contrast_type='t') + contrast = compute_contrast(labels, estimates, contrast_val, contrast_type='t') z_score = contrast.z_score() - + # Plot the result plotting.plot_surf_stat_map( fsaverage.infl_left, z_score, hemi='left', title=contrast_id, colorbar=True, From 4c81d84d8e43b8c90d02a1d83916e98c0cb28260 Mon Sep 17 00:00:00 2001 From: bthirion Date: Tue, 2 Oct 2018 23:42:57 +0200 Subject: [PATCH 104/210] remvoed localizer analysis as it is redundant with tutorial example --- .../plot_localizer_analysis.py | 87 ------------------- 1 file changed, 87 deletions(-) delete mode 100644 examples/02_first_level_models/plot_localizer_analysis.py diff --git a/examples/02_first_level_models/plot_localizer_analysis.py b/examples/02_first_level_models/plot_localizer_analysis.py deleted file mode 100644 index 88681f53..00000000 --- a/examples/02_first_level_models/plot_localizer_analysis.py +++ /dev/null @@ -1,87 +0,0 @@ -""" -First level analysis of localizer dataset -========================================= - -Full step-by-step example of fitting a GLM to experimental data and visualizing -the results. - -More specifically: - -1. A sequence of fMRI volumes are loaded -2. A design matrix describing all the effects related to the data is computed -3. a mask of the useful brain volume is computed -4. A GLM is applied to the dataset (effect/covariance, - then contrast estimation) - -""" -import numpy as np -import pandas as pd -from nilearn import plotting - -from nistats.first_level_model import FirstLevelModel -from nistats import datasets - -######################################################################### -# Prepare data and analysis parameters -# ------------------------------------- -# Prepare timing -t_r = 2.4 -slice_time_ref = 0.5 - -# Prepare data -data = datasets.fetch_localizer_first_level() -paradigm_file = data.paradigm -paradigm = pd.read_csv(paradigm_file, sep=' ', header=None, index_col=None) -paradigm.columns = ['session', 'trial_type', 'onset'] -fmri_img = data.epi_img - -######################################################################### -# Perform first level analysis -# ---------------------------- -# Setup and fit GLM -first_level_model = FirstLevelModel(t_r, slice_time_ref, - hrf_model='glover + derivative') -first_level_model = first_level_model.fit(fmri_img, paradigm) - -######################################################################### -# Estimate contrasts -# ------------------ -# Specify the contrasts -design_matrix = first_level_model.design_matrices_[0] -contrast_matrix = np.eye(design_matrix.shape[1]) -contrasts = dict([(column, contrast_matrix[i]) - for i, column in enumerate(design_matrix.columns)]) - -contrasts["audio"] = contrasts["clicDaudio"] + contrasts["clicGaudio"] +\ - contrasts["calculaudio"] + contrasts["phraseaudio"] -contrasts["video"] = contrasts["clicDvideo"] + contrasts["clicGvideo"] + \ - contrasts["calculvideo"] + contrasts["phrasevideo"] -contrasts["computation"] = contrasts["calculaudio"] + contrasts["calculvideo"] -contrasts["sentences"] = contrasts["phraseaudio"] + contrasts["phrasevideo"] - -######################################################################### -# Short list of more relevant contrasts -contrasts = { - "left-right": (contrasts["clicGaudio"] + contrasts["clicGvideo"] - - contrasts["clicDaudio"] - contrasts["clicDvideo"]), - "H-V": contrasts["damier_H"] - contrasts["damier_V"], - "audio-video": contrasts["audio"] - contrasts["video"], - "video-audio": -contrasts["audio"] + contrasts["video"], - "computation-sentences": (contrasts["computation"] - - contrasts["sentences"]), - "reading-visual": contrasts["phrasevideo"] - contrasts["damier_H"] - } - -######################################################################### -# contrast estimation -for index, (contrast_id, contrast_val) in enumerate(contrasts.items()): - print(' Contrast % 2i out of %i: %s' % - (index + 1, len(contrasts), contrast_id)) - z_map = first_level_model.compute_contrast(contrast_val, - output_type='z_score') - - # Create snapshots of the contrasts - display = plotting.plot_stat_map(z_map, display_mode='z', - threshold=3.0, title=contrast_id) - -plotting.show() From 74261e63da4e0b4b0e9cb6147c4787ef0aeb1662 Mon Sep 17 00:00:00 2001 From: bthirion Date: Tue, 2 Oct 2018 23:43:29 +0200 Subject: [PATCH 105/210] Make the SPM multimodal faces example more pedagogical --- .../plot_spm_multimodal_faces.py | 102 ++++++++++++------ 1 file changed, 67 insertions(+), 35 deletions(-) diff --git a/examples/02_first_level_models/plot_spm_multimodal_faces.py b/examples/02_first_level_models/plot_spm_multimodal_faces.py index 22e94f44..ec39ed65 100644 --- a/examples/02_first_level_models/plot_spm_multimodal_faces.py +++ b/examples/02_first_level_models/plot_spm_multimodal_faces.py @@ -1,6 +1,13 @@ -""" -Minimal script for preprocessing single-subject data (two session) -================================================================== +"""Single-subject data (two sessions) in native space +================================================== + +The example shows the analysis of an SPM dataset studying face perception. +The anaylsis is performed in native spave. Realignment parameters are provided +with the input images, but those have not been resampled to a common space. + +The experimental paradigm is simple, with two conditions; viewing a +face image or a scrambled face image, supposedly with the same +low-level statistical properties, to find face-specific responses. For details on the data, please see: Henson, R.N., Goshen-Gottstein, Y., Ganel, T., Otten, L.J., Quayle, A., @@ -8,37 +15,32 @@ perception, recognition and priming. Cereb Cortex. 2003 Jul;13(7):793-805. http://www.dx.doi.org/10.1093/cercor/13.7.793 -Note: this example takes a lot of time because the input are lists of 3D images +This example takes a lot of time because the input are lists of 3D images sampled in different position (encoded by different) affine functions. + """ print(__doc__) -# Standard imports -import numpy as np -from scipy.io import loadmat -import pandas as pd - -# Imports for GLM -from nilearn.image import concat_imgs, resample_img, mean_img -from nistats.design_matrix import make_design_matrix -from nistats.first_level_model import FirstLevelModel -from nistats.datasets import fetch_spm_multimodal_fmri ######################################################################### # Fetch spm multimodal_faces data +from nistats.datasets import fetch_spm_multimodal_fmri subject_data = fetch_spm_multimodal_fmri() ######################################################################### -# Experimental paradigm specification -tr = 2. -slice_time_ref = 0. -drift_model = 'Cosine' -hrf_model = 'spm + derivative' -period_cut = 128. +# Timing and design matrix parameter specification +tr = 2. # repetition time, in seconds +slice_time_ref = 0. # we will sample the design matrix at the beggining of each acquisition +drift_model = 'Cosine' # We use a discrete cosin transform to model signal drifts. +period_cut = 128. # The cutoff for the drift model is 1/128 Hz. +hrf_model = 'spm + derivative' # The hemodunamic response finction is the SPM canonical one ######################################################################### -# Resample the images +# Resample the images. +# +# This is achieved by the concat_imgs function of Nilearn. +from nilearn.image import concat_imgs, resample_img, mean_img fmri_img = [concat_imgs(subject_data.func1, auto_resample=True), concat_imgs(subject_data.func2, auto_resample=True)] affine, shape = fmri_img[0].affine, fmri_img[0].shape @@ -51,59 +53,89 @@ ######################################################################### # Make design matrices +import numpy as np +from scipy.io import loadmat +import pandas as pd +from nistats.design_matrix import make_design_matrix design_matrices = [] + +######################################################################### +# loop over the two sessions for idx in range(len(fmri_img)): - # Build paradigm + # The following manipulations are meant to build a valid events descriptor + # define the onset times of the vents n_scans = fmri_img[idx].shape[-1] timing = loadmat(getattr(subject_data, "trials_ses%i" % (idx + 1)), squeeze_me=True, struct_as_record=False) - faces_onsets = timing['onsets'][0].ravel() scrambled_onsets = timing['onsets'][1].ravel() onsets = np.hstack((faces_onsets, scrambled_onsets)) onsets *= tr # because onsets were reporting in 'scans' units + + # define the events trial type conditions = (['faces'] * len(faces_onsets) + ['scrambled'] * len(scrambled_onsets)) + # make a paradigm out of it paradigm = pd.DataFrame({'trial_type': conditions, 'onset': onsets}) - - # Build design matrix + + # Define the sampling times for the design matrix frame_times = np.arange(n_scans) * tr + # Build design matrix with the reviously defined parameters design_matrix = make_design_matrix( frame_times, paradigm, hrf_model=hrf_model, drift_model=drift_model, period_cut=period_cut) + + # put the design matrices in a list design_matrices.append(design_matrix) ######################################################################### -# We can specify basic contrasts (To get beta maps) +# We can specify basic contrasts (to get beta maps). +# We start by specifying canonical contrast that isolate design matrix columns contrast_matrix = np.eye(design_matrix.shape[1]) -contrasts = dict([(column, contrast_matrix[i]) +basic_contrasts = dict([(column, contrast_matrix[i]) for i, column in enumerate(design_matrix.columns)]) + ######################################################################### -# Instead in this example we define more interesting contrasts +# We actually want more interesting contrasts +# The simplest contrast just makes the difference between the two main conditions. +# We define the two opposite versions to run one-tail t-tests. +# We also define the effects of interest contrast, a 2-dimensional contrasts spanning the two conditions. contrasts = { - 'faces-scrambled': contrasts['faces'] - contrasts['scrambled'], - 'scrambled-faces': -contrasts['faces'] + contrasts['scrambled'], - 'effects_of_interest': np.vstack((contrasts['faces'], - contrasts['scrambled'])) + 'faces-scrambled': basic_contrasts['faces'] - basic_contrasts['scrambled'], + 'scrambled-faces': -basic_contrasts['faces'] + basic_contrasts['scrambled'], + 'effects_of_interest': np.vstack((basic_contrasts['faces'], + basic_contrasts['scrambled'])) } ######################################################################### -# Fit GLM +# Fit the GLM -- 2 sessions. +# Imports for GLM, the sepcify, then fit. +from nistats.first_level_model import FirstLevelModel print('Fitting a GLM') -fmri_glm = FirstLevelModel(tr, slice_time_ref) +fmri_glm = FirstLevelModel() fmri_glm = fmri_glm.fit(fmri_img, design_matrices=design_matrices) ######################################################################### -# Compute contrast maps +# Compute contrast-related statistical maps (in z-scale), and plot them print('Computing contrasts') from nilearn import plotting +# Iterate on contrasts for contrast_id, contrast_val in contrasts.items(): print("\tcontrast id: %s" % contrast_id) + # compute the contrasts z_map = fmri_glm.compute_contrast( contrast_val, output_type='z_score') + # plot the contrasts as soon as they're generated + # the display is overlayed on the mean fMRI image + # a threshold of 3.0 is used. More sophisticated choices are possible. plotting.plot_stat_map( z_map, bg_img=mean_image, threshold=3.0, display_mode='z', cut_coords=3, black_bg=True, title=contrast_id) +######################################################################### +# Show the resulting maps plotting.show() + +######################################################################### +# We observe that the analysis results in wide activity for the 'effects of interest' contrast, showing the implications of large portions of the visual cortex in the conditions. By contrast, the differential effect between "faces" and "scambled" involves sparser, more anterior and lateral regions. It displays also some responses in the frontal lobe. From 2ca1c4e287a782ad83d8ed86fbd32d144c1698c2 Mon Sep 17 00:00:00 2001 From: bthirion Date: Wed, 3 Oct 2018 00:06:49 +0200 Subject: [PATCH 106/210] Small doc improvements on examples --- .../plot_spm_multimodal_faces.py | 6 +- examples/03_second_level_models/plot_oasis.py | 61 +++++++++++-------- 2 files changed, 39 insertions(+), 28 deletions(-) diff --git a/examples/02_first_level_models/plot_spm_multimodal_faces.py b/examples/02_first_level_models/plot_spm_multimodal_faces.py index ec39ed65..497a2408 100644 --- a/examples/02_first_level_models/plot_spm_multimodal_faces.py +++ b/examples/02_first_level_models/plot_spm_multimodal_faces.py @@ -134,8 +134,8 @@ cut_coords=3, black_bg=True, title=contrast_id) ######################################################################### -# Show the resulting maps +# Show the resulting maps: +# We observe that the analysis results in wide activity for the 'effects of interest' contrast, showing the implications of large portions of the visual cortex in the conditions. By contrast, the differential effect between "faces" and "scambled" involves sparser, more anterior and lateral regions. It displays also some responses in the frontal lobe. + plotting.show() -######################################################################### -# We observe that the analysis results in wide activity for the 'effects of interest' contrast, showing the implications of large portions of the visual cortex in the conditions. By contrast, the differential effect between "faces" and "scambled" involves sparser, more anterior and lateral regions. It displays also some responses in the frontal lobe. diff --git a/examples/03_second_level_models/plot_oasis.py b/examples/03_second_level_models/plot_oasis.py index 1793d3a0..209dc23a 100644 --- a/examples/03_second_level_models/plot_oasis.py +++ b/examples/03_second_level_models/plot_oasis.py @@ -19,64 +19,69 @@ from the OASIS dataset to limit the memory usage. Note that more power would be obtained from using a larger sample of subjects. -____ - """ # Authors: Bertrand Thirion, , July 2018 # Elvis Dhomatob, , Apr. 2014 # Virgile Fritsch, , Apr 2014 # Gael Varoquaux, Apr 2014 -import numpy as np -import matplotlib.pyplot as plt -from nilearn import datasets -from nilearn.input_data import NiftiMasker + n_subjects = 100 # more subjects requires more memory ############################################################################ # Load Oasis dataset -# ------------------- +# ------------------ + +from nilearn import datasets oasis_dataset = datasets.fetch_oasis_vbm(n_subjects=n_subjects) gray_matter_map_filenames = oasis_dataset.gray_matter_maps age = oasis_dataset.ext_vars['age'].astype(float) -# sex is encoded as 'M' or 'F'. make it a binary variable +############################################################################### +# Sex is encoded as 'M' or 'F'. make it a binary variable sex = oasis_dataset.ext_vars['mf'] == b'F' -# print basic information on the dataset +############################################################################### +# Print basic information on the dataset print('First gray-matter anatomy image (3D) is located at: %s' % oasis_dataset.gray_matter_maps[0]) # 3D data print('First white-matter anatomy image (3D) is located at: %s' % oasis_dataset.white_matter_maps[0]) # 3D data -## get a mask image +############################################################################### +# Get a mask image: A mask of the cortex of the ISBM template gm_mask = datasets.fetch_icbm152_brain_gm_mask() -## Since this mask has a different resolution +############################################################################### +# Resample the images, since this mask has a different resolution from nilearn.image import resample_to_img mask_img = resample_to_img( gm_mask, gray_matter_map_filenames[0], interpolation='nearest') ############################################################################# # Analyse data -# ---------------- - -from nistats.second_level_model import SecondLevelModel +# ------------ +# +# First create an adequate design matrix with three columns: 'age', +# 'sex', 'intercept'. import pandas as pd - +import numpy as np intercept = np.ones(n_subjects) design_matrix = pd.DataFrame(np.vstack((age, sex, intercept)).T, columns=['age', 'sex', 'intercept']) -# plot the design matrix + +############################################################################# +# Plot the design matrix from nistats.reporting import plot_design_matrix ax = plot_design_matrix(design_matrix) ax.set_title('Second level design matrix', fontsize=12) ax.set_ylabel('maps') -plt.tight_layout() ########################################################################## -# specify and fit the model +# Specify and fit the second-level model when loading the data, we +# smooth a little bit tom improve statistical behavior +from nistats.second_level_model import SecondLevelModel second_level_model = SecondLevelModel(smoothing_fwhm=2.0, mask=mask_img) second_level_model.fit(gray_matter_map_filenames, design_matrix=design_matrix) @@ -88,19 +93,25 @@ output_type='z_score') ########################################################################### -# We threshold the second level contrast at uncorrected p < 0.001 and plot -from nilearn import plotting +# We threshold the second level contrast at uncorrected p < 0.001 and plot it. +# First compute the threshold. from nistats.thresholding import map_threshold _, threshold = map_threshold( z_map, threshold=.05, height_control='fdr') +print('The FDR=.05-corrected threshold is: %.3g' % threshold) +########################################################################### +# The plot it +from nilearn import plotting display = plotting.plot_stat_map( z_map, threshold=threshold, colorbar=True, display_mode='z', cut_coords=[-4, 26], - title='age effect on grey matter density (FDR < .05)') + title='age effect on grey matter density (FDR = .05)') +plotting.show() ########################################################################### -# Can also study the effect of sex +# Can also study the effect of sex: compute the stat, compute the +# threshold, plot the map z_map = second_level_model.compute_contrast(second_level_contrast='sex', output_type='z_score') @@ -108,8 +119,8 @@ z_map, threshold=.05, height_control='fdr') plotting.plot_stat_map( z_map, threshold=threshold, colorbar=True, - title='sex effect on grey matter density (FDR < .05)') + title='sex effect on grey matter density (FDR = .05)') -plotting.show() ########################################################################### -# Note that there is no significant effect of sex on grey matter density. +# Note that there does not seem to be any significant effect of sex on grey matter density on that dataset. + From 548322199f8ca9e584d875e28180bb61305cc40a Mon Sep 17 00:00:00 2001 From: bthirion Date: Wed, 3 Oct 2018 09:08:54 +0200 Subject: [PATCH 107/210] A bunch of typose --- .../02_first_level_models/plot_fiac_analysis.py | 17 +++++++++++++---- .../plot_localizer_surface_analysis.py | 2 +- .../plot_spm_multimodal_faces.py | 2 +- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/examples/02_first_level_models/plot_fiac_analysis.py b/examples/02_first_level_models/plot_fiac_analysis.py index 29f77d51..1eb6f4b2 100644 --- a/examples/02_first_level_models/plot_fiac_analysis.py +++ b/examples/02_first_level_models/plot_fiac_analysis.py @@ -46,7 +46,7 @@ fmri_img = [data['func1'], data['func2']] ######################################################################### -# Create a mean image for plotting puepose +# Create a mean image for plotting purpose from nilearn.image import mean_img mean_img_ = mean_img(fmri_img[0]) @@ -104,14 +104,23 @@ def pad_vector(contrast_, n_columns): contrast_val, output_type='z_score') # Write the resulting stat images to file - z_image_path = path.join(write_dir, '%s_z_map.nii' % contrast_id) + z_image_path = path.join(write_dir, '%s_z_map.nii.gz' % contrast_id) z_map.to_filename(z_image_path) ######################################################################### -# make a snapshot of the 'Effects_of_interest' contrast map +# make a snapshot of the 'Effects_of_interest' contrast map. +# We first compute a threshold corresponding to an FDR correction of .05 +# We also discard isolated sets of less that 10 voxels +from nistats.thresholding import map_threshold zmap = path.join(write_dir, 'Effects_of_interest_z_map.nii') +thresholded_map, threshold = map_threshold( + zmap, height_control='fdr', threshold=.05, cluster_threshold=10) + +######################################################################### +# Then display the map display = plotting.plot_stat_map( - zmap, bg_img=mean_img_, threshold=2.5, title=contrast_id) + thresholded_map, bg_img=mean_img_, threshold=threshold, + title='%s, fdr=.05' % 'Effects of interest') ######################################################################### # We can save the figure a posteriori diff --git a/examples/02_first_level_models/plot_localizer_surface_analysis.py b/examples/02_first_level_models/plot_localizer_surface_analysis.py index 53776f54..ef33014e 100644 --- a/examples/02_first_level_models/plot_localizer_surface_analysis.py +++ b/examples/02_first_level_models/plot_localizer_surface_analysis.py @@ -78,7 +78,7 @@ # Create the design matrix # # We specify an hrf model containing Glover model and its time derivative -# the drift model is implicitly a cosine basis with period cutodd 128s. +# the drift model is implicitly a cosine basis with period cutoff 128s. from nistats.design_matrix import make_design_matrix design_matrix = make_design_matrix( frame_times, paradigm=paradigm, hrf_model='glover + derivative') diff --git a/examples/02_first_level_models/plot_spm_multimodal_faces.py b/examples/02_first_level_models/plot_spm_multimodal_faces.py index 497a2408..81e28799 100644 --- a/examples/02_first_level_models/plot_spm_multimodal_faces.py +++ b/examples/02_first_level_models/plot_spm_multimodal_faces.py @@ -63,7 +63,7 @@ # loop over the two sessions for idx in range(len(fmri_img)): # The following manipulations are meant to build a valid events descriptor - # define the onset times of the vents + # define the onset times of the events n_scans = fmri_img[idx].shape[-1] timing = loadmat(getattr(subject_data, "trials_ses%i" % (idx + 1)), squeeze_me=True, struct_as_record=False) From c1e0220f1de0f4a6b5b2a3bee3e356de1904e41b Mon Sep 17 00:00:00 2001 From: bthirion Date: Wed, 3 Oct 2018 13:55:37 +0200 Subject: [PATCH 108/210] Misc improvements of second-level examples --- .../plot_fiac_analysis.py | 51 +++++++++++------ examples/03_second_level_models/plot_oasis.py | 2 +- .../plot_second_level_one_sample_test.py | 34 +++++------ .../plot_second_level_two_sample_test.py | 28 ++++++--- .../plot_thresholding.py | 57 ++++++++++++++----- 5 files changed, 117 insertions(+), 55 deletions(-) diff --git a/examples/02_first_level_models/plot_fiac_analysis.py b/examples/02_first_level_models/plot_fiac_analysis.py index 1eb6f4b2..2f4e6c2b 100644 --- a/examples/02_first_level_models/plot_fiac_analysis.py +++ b/examples/02_first_level_models/plot_fiac_analysis.py @@ -60,7 +60,7 @@ ######################################################################### # GLM estimation # ---------------------------------- -# GLM specification +# GLM specification. Note that the mask was provided in the dataset. So we use it. from nistats.first_level_model import FirstLevelModel fmri_glm = FirstLevelModel(mask=data['mask'], minimize_memory=True) @@ -102,29 +102,48 @@ def pad_vector(contrast_, n_columns): # note that the model implictly compute a fixed effects across the two sessions z_map = fmri_glm.compute_contrast( contrast_val, output_type='z_score') - + # Write the resulting stat images to file z_image_path = path.join(write_dir, '%s_z_map.nii.gz' % contrast_id) z_map.to_filename(z_image_path) ######################################################################### -# make a snapshot of the 'Effects_of_interest' contrast map. -# We first compute a threshold corresponding to an FDR correction of .05 -# We also discard isolated sets of less that 10 voxels -from nistats.thresholding import map_threshold -zmap = path.join(write_dir, 'Effects_of_interest_z_map.nii') -thresholded_map, threshold = map_threshold( - zmap, height_control='fdr', threshold=.05, cluster_threshold=10) +# Comparing session-specific and fixed effects. +# Here, we compare the activation mas produced from each separately then, the fixed effects version + +contrast_id = 'Effects_of_interest' + +######################################################################### +# Statistics for the first session + +fmri_glm = fmri_glm.fit(fmri_img[0], design_matrices=design_matrices[0]) +z_map = fmri_glm.compute_contrast( + contrasts[contrast_id], output_type='z_score') +plotting.plot_stat_map( + z_map, bg_img=mean_img_, threshold=3.0, + title='%s, first session' % contrast_id) + +######################################################################### +# Statistics for the second session + +fmri_glm = fmri_glm.fit(fmri_img[1], design_matrices=design_matrices[1]) +z_map = fmri_glm.compute_contrast( + contrasts[contrast_id], output_type='z_score') +plotting.plot_stat_map( + z_map, bg_img=mean_img_, threshold=3.0, + title='%s, second session' % contrast_id) ######################################################################### -# Then display the map -display = plotting.plot_stat_map( - thresholded_map, bg_img=mean_img_, threshold=threshold, - title='%s, fdr=.05' % 'Effects of interest') +# Fixed effects statistics + +fmri_glm = fmri_glm.fit(fmri_img, design_matrices=design_matrices) +z_map = fmri_glm.compute_contrast( + contrasts[contrast_id], output_type='z_score') +plotting.plot_stat_map( + z_map, bg_img=mean_img_, threshold=3.0, + title='%s, fixed effects' % contrast_id) ######################################################################### -# We can save the figure a posteriori -display.savefig(path.join(write_dir, '%s_z_map.png' % contrast_id)) +# Not unexpectedly, the fixed effects version looks displays higher peaks than the input sessions. Computing fixed effects enhances the signal-to-noise ratio of the resulting brain maps -print('All the results were witten in %s' % write_dir) plotting.show() diff --git a/examples/03_second_level_models/plot_oasis.py b/examples/03_second_level_models/plot_oasis.py index 209dc23a..964cc8a2 100644 --- a/examples/03_second_level_models/plot_oasis.py +++ b/examples/03_second_level_models/plot_oasis.py @@ -87,7 +87,7 @@ design_matrix=design_matrix) ########################################################################## -# To estimate the contrast is very simple. We can just provide the column +# Estimate the contrast is very simple. We can just provide the column # name of the design matrix. z_map = second_level_model.compute_contrast(second_level_contrast=[1, 0, 0], output_type='z_score') diff --git a/examples/03_second_level_models/plot_second_level_one_sample_test.py b/examples/03_second_level_models/plot_second_level_one_sample_test.py index d4cf61d5..30b29d41 100644 --- a/examples/03_second_level_models/plot_second_level_one_sample_test.py +++ b/examples/03_second_level_models/plot_second_level_one_sample_test.py @@ -1,9 +1,9 @@ """ -GLM fitting in second level fMRI -================================ +Second-level fMRI model: one sample test +======================================== -Full step-by-step example of fitting a GLM to perform a second level analysis -in experimental data and visualizing the results. +Full step-by-step example of fitting a GLM to perform a second-level analysis (one-sample test) +and visualizing the results. More specifically: @@ -11,23 +11,16 @@ 2. a mask of the useful brain volume is computed 3. A one-sample t-test is applied to the brain maps -(as fixed effects, then contrast estimation) +We focus on a given contrast of the localizer dataset: the motor response to left versus right button press. Both at the ndividual and group level, this is expected to elicit activity in the motor cortex (positive in the right hemisphere, negative in the left hemisphere). """ -import pandas as pd -from nilearn import plotting -from scipy.stats import norm -import matplotlib.pyplot as plt - -from nilearn.datasets import fetch_localizer_contrasts -from nistats.second_level_model import SecondLevelModel - ######################################################################### # Fetch dataset # -------------- # We download a list of left vs right button press contrasts from a -# localizer dataset. +# localizer dataset. Note that we fetc individual t-maps that represent the Bold activity estimate divided by the uncertainty about this estimate. +from nilearn.datasets import fetch_localizer_contrasts n_subjects = 16 data = fetch_localizer_contrasts(["left vs right button press"], n_subjects, get_tmaps=True) @@ -38,6 +31,8 @@ # We plot a grid with all the subjects t-maps thresholded at t = 2 for # simple visualization purposes. The button press effect is visible among # all subjects +from nilearn import plotting +import matplotlib.pyplot as plt subjects = [subject_data[0] for subject_data in data['ext_vars']] fig, axes = plt.subplots(nrows=4, ncols=4) for cidx, tmap in enumerate(data['tmaps']): @@ -53,10 +48,14 @@ # --------------------------- # We define the input maps and the design matrix for the second level model # and fit it. +import pandas as pd second_level_input = data['cmaps'] design_matrix = pd.DataFrame([1] * len(second_level_input), columns=['intercept']) +############################################################################ +# Model specification and fit +from nistats.second_level_model import SecondLevelModel second_level_model = SecondLevelModel(smoothing_fwhm=8.0) second_level_model = second_level_model.fit(second_level_input, design_matrix=design_matrix) @@ -68,10 +67,13 @@ ########################################################################### # We threshold the second level contrast at uncorrected p < 0.001 and plot +from scipy.stats import norm p_val = 0.001 -z_th = norm.isf(p_val) +p001_unc = norm.isf(p_val) display = plotting.plot_glass_brain( - z_map, threshold=z_th, colorbar=True, plot_abs=False, display_mode='z', + z_map, threshold=p001_unc, colorbar=True, display_mode='z', plot_abs=False, title='group left-right button press (unc p<0.001') +########################################################################### +# As expected, we find the motor cortex plotting.show() diff --git a/examples/03_second_level_models/plot_second_level_two_sample_test.py b/examples/03_second_level_models/plot_second_level_two_sample_test.py index 8bbe88cc..b9b3eba5 100644 --- a/examples/03_second_level_models/plot_second_level_two_sample_test.py +++ b/examples/03_second_level_models/plot_second_level_two_sample_test.py @@ -1,15 +1,16 @@ """ -GLM fitting in second level fMRI -================================ +Second-level fMRI model: a two-sample test +========================================== Full step-by-step example of fitting a GLM to perform a second level analysis in experimental data and visualizing the results. More specifically: -1. A sequence of subject fMRI button press images is downloaded. -2. A two-sample t-test is applied to the brain maps -to see the effect of the contrast difference across subjects. +1. A sample of n=16 visual activity fMRIs are downloaded. +2. A two-sample t-test is applied to the brain maps in order to see the effect of the contrast difference across subjects. + +The contrast is between reponses to vertical versus horizontal checkerboards than are retinotopically distinct. At the individual level, these stimuli are sometimes used to map the borders of primary visual areas. At the group level, such a mapping is not possible. Yet, we may observe some significant effects in these areas. """ import pandas as pd @@ -28,7 +29,7 @@ ["horizontal checkerboard"], n_subjects, get_tmaps=True) # What remains implicit here is that there is a one-to-one -# correspondence between the two sample: the first image of both +# correspondence between the two samples: the first image of both # samples comes from subject S1, the second from subject S2 etc. ############################################################################ @@ -38,29 +39,35 @@ # and fit it. second_level_input = sample_vertical['cmaps'] + sample_horizontal['cmaps'] +############################################################################ # model the effect of conditions (sample 1 vs sample 2) import numpy as np condition_effect = np.hstack(([1] * n_subjects, [- 1] * n_subjects)) +############################################################################ # model the subject effect: each subject is observed in sample 1 and sample 2 subject_effect = np.vstack((np.eye(n_subjects), np.eye(n_subjects))) subjects = ['S%02d' % i for i in range(1, n_subjects + 1)] + +############################################################################ +# Assemble those in a design matrix design_matrix = pd.DataFrame( np.hstack((condition_effect[:, np.newaxis], subject_effect)), columns=['vertical vs horizontal'] + subjects) +############################################################################ # plot the design_matrix from nistats.reporting import plot_design_matrix plot_design_matrix(design_matrix) +############################################################################ # formally specify the analysis model and fit it from nistats.second_level_model import SecondLevelModel -second_level_model = SecondLevelModel() -second_level_model = second_level_model.fit( +second_level_model = SecondLevelModel().fit( second_level_input, design_matrix=design_matrix) ########################################################################## -# To estimate the contrast is very simple. We can just provide the column +# Estimating the contrast is very simple. We can just provide the column # name of the design matrix. z_map = second_level_model.compute_contrast('vertical vs horizontal', output_type='z_score') @@ -72,4 +79,7 @@ z_map, threshold=threshold, colorbar=True, plot_abs=False, title='vertical vs horizontal checkerboard (unc p<0.001') +########################################################################### +# Unsurprisingly, we see activity in the primary visual cortex, both positive and negative. + plotting.show() diff --git a/examples/03_second_level_models/plot_thresholding.py b/examples/03_second_level_models/plot_thresholding.py index a956798d..b5065b95 100644 --- a/examples/03_second_level_models/plot_thresholding.py +++ b/examples/03_second_level_models/plot_thresholding.py @@ -1,41 +1,45 @@ """ -Example of simple second level analysis -======================================= +Statistical testing of a second-level analysis +============================================== -Perform a one-sample t-test on a bunch of images -(a.k.a. second-level analyis in fMRI) and threshold a statistical image. -This is based on the so-called localizer dataset. +Perform a one-sample t-test on a bunch of images (a.k.a. second-level analyis in fMRI) and threshold the resulting statistical map. + +This example is based on the so-called localizer dataset. It shows activation related to a mental computation task, as opposed to narrative sentence reading/listening. """ -from nilearn import datasets -from nilearn.input_data import NiftiMasker ######################################################################### # Prepare some images for a simple t test # ---------------------------------------- # This is a simple manually performed second level analysis +from nilearn import datasets n_samples = 20 localizer_dataset = datasets.fetch_localizer_calculation_task( n_subjects=n_samples) -# mask data -nifti_masker = NiftiMasker( - smoothing_fwhm=5, - memory='nilearn_cache', memory_level=1) # cache options +######################################################################### +# Get the set of individual statstical maps (contrast estimates) cmap_filenames = localizer_dataset.cmaps ######################################################################### # Perform the second level analysis # ---------------------------------- -# perform a one-sample test on these values +# +# First define a design matrix for the model. As the model is trivial (one-sample test), the design matrix is just one column with ones. import pandas as pd design_matrix = pd.DataFrame([1] * n_samples, columns=['intercept']) +######################################################################### +# Specify and estimate the model from nistats.second_level_model import SecondLevelModel second_level_model = SecondLevelModel().fit( cmap_filenames, design_matrix=design_matrix) + +######################################################################### +# Compute the only possible contrast: the one-sample test. Since there +# is only one possible contrast, we don't need to specify it in detail z_map = second_level_model.compute_contrast(output_type='z_score') ######################################################################### @@ -46,19 +50,46 @@ z_map, threshold=.001, height_control='fpr', cluster_threshold=10) ######################################################################### -# Now use FDR <.05, no cluster-level threshold +# Now use FDR <.05, (False Discovery Rate) no cluster-level threshold thresholded_map2, threshold2 = map_threshold( z_map, threshold=.05, height_control='fdr') +print('The FDR=.05 threshold is %.3g' % threshold2) + +######################################################################### +# Now use FWER <.05, (Familywise Error Rate) no cluster-level threshold +# As the data have not been intensively smoothed, we can use a simple Bonferroni correction +thresholded_map3, threshold3 = map_threshold( + z_map, threshold=.05, height_control='bonferroni') +print('The p<.05 Bonferroni-corrected threshold is %.3g' % threshold3) ######################################################################### # Visualize the results +# --------------------- +# +# First the unthresholded map from nilearn import plotting display = plotting.plot_stat_map(z_map, title='Raw z map') + +######################################################################### +# Second the p<.001 uncorrected-thresholded map (with only clusters > 10 voxels) plotting.plot_stat_map( thresholded_map1, cut_coords=display.cut_coords, threshold=threshold1, title='Thresholded z map, fpr <.001, clusters > 10 voxels') + +######################################################################### +# Third the fdr-thresholded map plotting.plot_stat_map(thresholded_map2, cut_coords=display.cut_coords, title='Thresholded z map, expected fdr = .05', threshold=threshold2) +######################################################################### +# Fourth the Bonferroni-thresholded map +plotting.plot_stat_map(thresholded_map3, cut_coords=display.cut_coords, + title='Thresholded z map, expected fwer < .05', + threshold=threshold3) + +######################################################################### +# These different thresholds correpond to different statistical guarnatees: +# in the FWER corrected image there is only a probability<.05 of observing any false positive voxel. In the FDR-corrected image, 5% of the voxels found are likely to be false positive. In the uncorrected image, one expects a few tens of alse positive voxels. + plotting.show() From 2077f4907d783045b15fff60c2faac26e3bdde0e Mon Sep 17 00:00:00 2001 From: bthirion Date: Wed, 3 Oct 2018 18:15:57 +0200 Subject: [PATCH 109/210] some changes on examples details --- .../plot_design_matrix.py | 33 ++++++--- examples/04_low_level_functions/plot_hrf.py | 45 +++++++++---- .../plot_second_level_design_matrix.py | 21 +++--- .../write_paradigm_file.py | 67 +++++++++++++------ 4 files changed, 112 insertions(+), 54 deletions(-) diff --git a/examples/04_low_level_functions/plot_design_matrix.py b/examples/04_low_level_functions/plot_design_matrix.py index dfe811c5..d7dc9088 100644 --- a/examples/04_low_level_functions/plot_design_matrix.py +++ b/examples/04_low_level_functions/plot_design_matrix.py @@ -9,52 +9,59 @@ Requires matplotlib """ -import numpy as np try: import matplotlib.pyplot as plt except ImportError: raise RuntimeError("This script needs the matplotlib library") -from nistats.design_matrix import make_design_matrix -from nistats.reporting import plot_design_matrix -import pandas as pd - - ######################################################################### # Define parameters # ---------------------------------- # first we define parameters related to the images acquisition -tr = 1.0 -n_scans = 128 -frame_times = np.arange(n_scans) * tr +import numpy as np +tr = 1.0 # repetition time is 1 second +n_scans = 128 # the acquisition comprises 128 scans +frame_times = np.arange(n_scans) * tr # here are the corespoding frame times ######################################################################### # then we define parameters related to the experimental design +# these are the types of the different trials conditions = ['c0', 'c0', 'c0', 'c1', 'c1', 'c1', 'c3', 'c3', 'c3'] +# these are the corresponding onset times onsets = [30., 70., 100., 10., 30., 90., 30., 40., 60.] -motion = np.cumsum(np.random.randn(n_scans, 6), 0) # simulate motion +# Next, we simulate 6 motion parameters jointly observed with fMRI acquisitions +motion = np.cumsum(np.random.randn(n_scans, 6), 0) +# The 6 parameters correspond to three translations and three +# rotations describing rigid body motion add_reg_names = ['tx', 'ty', 'tz', 'rx', 'ry', 'rz'] ######################################################################### # Create design matrices # ------------------------------------- # The same parameters allow us to obtain a variety of design matrices -# First we compute an event-related design matrix +# We first create an event object +import pandas as pd paradigm = pd.DataFrame({'trial_type': conditions, 'onset': onsets}) +######################################################################### +# We sample the paradigm into a design matrix, also including additional regressors hrf_model = 'glover' +from nistats.design_matrix import make_design_matrix X1 = make_design_matrix( frame_times, paradigm, drift_model='polynomial', drift_order=3, add_regs=motion, add_reg_names=add_reg_names, hrf_model=hrf_model) ######################################################################### # Now we compute a block design matrix. We add duration to create the blocks. +# For this we first define an event structure that includes the duration parameter duration = 7. * np.ones(len(conditions)) paradigm = pd.DataFrame({'trial_type': conditions, 'onset': onsets, 'duration': duration}) +######################################################################### +# Then we sample the design matrix X2 = make_design_matrix(frame_times, paradigm, drift_model='polynomial', drift_order=3, hrf_model=hrf_model) @@ -68,6 +75,7 @@ ######################################################################### # Here the three designs side by side +from nistats.reporting import plot_design_matrix fig, (ax1, ax2, ax3) = plt.subplots(figsize=(10, 6), nrows=1, ncols=3) plot_design_matrix(X1, ax=ax1) ax1.set_title('Event-related design matrix', fontsize=12) @@ -75,5 +83,8 @@ ax2.set_title('Block design matrix', fontsize=12) plot_design_matrix(X3, ax=ax3) ax3.set_title('FIR design matrix', fontsize=12) + +######################################################################### +# Improve the layout and show the result plt.subplots_adjust(left=0.08, top=0.9, bottom=0.21, right=0.96, wspace=0.3) plt.show() diff --git a/examples/04_low_level_functions/plot_hrf.py b/examples/04_low_level_functions/plot_hrf.py index 59f6a2db..6b1a96a6 100644 --- a/examples/04_low_level_functions/plot_hrf.py +++ b/examples/04_low_level_functions/plot_hrf.py @@ -1,38 +1,56 @@ -""" -Example of hemodynamic reponse functions. +"""Example of hemodynamic reponse functions. ========================================= -Plot the hrf model in SPM together with the hrf shape proposed by -G.Glover, as well as their time and dispersion derivatives. +Plot the hemodynamic reponse function (hrf) model in SPM together with +the hrf shape proposed by G.Glover, as well as their time and +dispersion derivatives. -Requires matplotlib +Requires matplotlib. -""" +The hrf is the filter that couples neural responses to the +metabolic-related changes in the MRI signal. hrf models are simply +phenomenological. -import numpy as np -import matplotlib.pyplot as plt -from nistats import hemodynamic_models +In current analysis frameworks, the choice of hrf model is essentially +left to the user. Fortunately, using spm or Glover model does not make +a huge difference. Adding derivatives should be considered whenever +timing information has some degree of uncertainty. It is actually +useful to detect timing issues. +""" ######################################################################### -# A first step: looking at our data -# ---------------------------------- +# Set up some parameters for model display +# ---------------------------------------- # -# Let's quickly plot this file: +# To get an impulse reponse, we simulate a single event occurring at time t=0, with duration 1s. +import numpy as np frame_times = np.linspace(0, 30, 61) onset, amplitude, duration = 0., 1., 1. +exp_condition = np.array((onset, duration, amplitude)).reshape(3, 1) + +######################################################################### +# Sample this on a fris for display stim = np.zeros_like(frame_times) stim[(frame_times > onset) * (frame_times <= onset + duration)] = amplitude -exp_condition = np.array((onset, duration, amplitude)).reshape(3, 1) + +######################################################################### +# Define the candidate hrf models hrf_models = [None, 'glover + derivative', 'glover + derivative + dispersion'] ######################################################################### # sample the hrf +# -------------- +from nistats import hemodynamic_models +import matplotlib.pyplot as plt + fig = plt.figure(figsize=(9, 4)) for i, hrf_model in enumerate(hrf_models): + # obtain the signal of interest by convolution signal, name = hemodynamic_models.compute_regressor( exp_condition, hrf_model, frame_times, con_id='main', oversampling=16) + # plot this plt.subplot(1, 3, i + 1) plt.fill(frame_times, stim, 'k', alpha=.5, label='stimulus') for j in range(signal.shape[1]): @@ -41,5 +59,6 @@ plt.legend(loc=1) plt.title(hrf_model) +# adjust the plot plt.subplots_adjust(bottom=.12) plt.show() diff --git a/examples/04_low_level_functions/plot_second_level_design_matrix.py b/examples/04_low_level_functions/plot_second_level_design_matrix.py index b3205e75..628a9037 100644 --- a/examples/04_low_level_functions/plot_second_level_design_matrix.py +++ b/examples/04_low_level_functions/plot_second_level_design_matrix.py @@ -1,7 +1,14 @@ -""" -Example of second level design matrix +"""Example of second level design matrix ===================================== +The shows how a second-level design matrix is specified: assuming that +the data refer to a group of individuals, with one image per subject, +the design matrix typically holds the characteristics of each +individual. + +This is used in a second-level analysis to assess the impact of these +characteristics on brain signals. + Requires matplotlib. """ @@ -11,10 +18,6 @@ except ImportError: raise RuntimeError("This script needs the matplotlib library") -from nistats.design_matrix import create_second_level_design -from nistats.reporting import plot_design_matrix -import pandas as pd - ######################################################################### # Create a simple experimental paradigm # -------------------------------------- @@ -25,18 +28,20 @@ ############################################################################## # Specify extra information about the subjects to create confounders # Without confounders the design matrix would correspond to a one sample test +import pandas as pd extra_info_subjects = pd.DataFrame({'subject_label': subjects_label, 'age': range(15, 15 + n_subjects), 'sex': [0, 1] * int(n_subjects / 2)}) - ######################################################################### # Create a second level design matrix # ----------------------------------- - +from nistats.design_matrix import create_second_level_design design_matrix = create_second_level_design(subjects_label, extra_info_subjects) +######################################################################### # plot the results +from nistats.reporting import plot_design_matrix ax = plot_design_matrix(design_matrix) ax.set_title('Second level design matrix', fontsize=12) ax.set_ylabel('maps') diff --git a/examples/04_low_level_functions/write_paradigm_file.py b/examples/04_low_level_functions/write_paradigm_file.py index fda58570..60a334e9 100644 --- a/examples/04_low_level_functions/write_paradigm_file.py +++ b/examples/04_low_level_functions/write_paradigm_file.py @@ -1,42 +1,65 @@ -""" -Example of a paradigm .csv file generation: the neurospin/localizer paradigm. +"""Example of a paradigm .csv file generation: the neurospin/localizer paradigm. ============================================================================= -See Pinel et al., BMC neuroscience 2007 for reference +The protocol described is the so-called "archi standard" localizer +event sequence. See Pinel et al., BMC neuroscience 2007 for reference """ print(__doc__) -import numpy as np -import pandas as pd - ######################################################################### -# onset times in milliseconds -time = np.array([ - 0., 2.4, 8.7, 11.4, 15., 18., 20.7, 23.7, 26.7, 29.7, 33., 35.4, 39., - 41.7, 44.7, 48., 56.4, 59.7, 62.4, 69., 71.4, 75., 83.4, 87., 89.7, - 96., 108., 116.7, 119.4, 122.7, 125.4, 131.4, 135., 137.7, 140.4, - 143.4, 146.7, 149.4, 153., 156., 159., 162., 164.4, 167.7, 170.4, - 173.7, 176.7, 188.4, 191.7, 195., 198., 201., 203.7, 207., 210., - 212.7, 215.7, 218.7, 221.4, 224.7, 227.7, 230.7, 234., 236.7, 246., - 248.4, 251.7, 254.7, 257.4, 260.4, 264., 266.7, 269.7, 275.4, 278.4, - 284.4, 288., 291., 293.4, 296.7]) +# Define the onset times in seconds. Those are typically extracted +# from the stimulation software used. +import numpy as np +onset = np.array([ + 0., 2.4, 8.7, 11.4, 15., 18., 20.7, 23.7, 26.7, 29.7, 33., 35.4, 39., + 41.7, 44.7, 48., 56.4, 59.7, 62.4, 69., 71.4, 75., 83.4, 87., 89.7, + 96., 108., 116.7, 119.4, 122.7, 125.4, 131.4, 135., 137.7, 140.4, + 143.4, 146.7, 149.4, 153., 156., 159., 162., 164.4, 167.7, 170.4, + 173.7, 176.7, 188.4, 191.7, 195., 198., 201., 203.7, 207., 210., + 212.7, 215.7, 218.7, 221.4, 224.7, 227.7, 230.7, 234., 236.7, 246., + 248.4, 251.7, 254.7, 257.4, 260.4, 264., 266.7, 269.7, 275.4, 278.4, + 284.4, 288., 291., 293.4, 296.7]) ######################################################################### -# corresponding onset types +# Associated trial types: these are numbered between 0 and 9, hence +# correspond to 10 different conditions trial_idx = np.array( [7, 7, 0, 2, 9, 4, 9, 3, 5, 9, 1, 6, 8, 8, 6, 6, 8, 0, 3, 4, 5, 8, 6, 2, 9, 1, 6, 5, 9, 1, 7, 8, 6, 6, 1, 2, 9, 0, 7, 1, 8, 2, 7, 8, 3, 6, 0, 0, 6, 8, 7, 7, 1, 1, 1, 5, 5, 0, 7, 0, 4, 2, 7, 9, 8, 0, 6, 3, 3, 7, 1, 0, 0, 4, 1, 9, 8, 4, 9, 9]) -condition_ids = ['damier_H', 'damier_V', 'clicDaudio', 'clicGaudio', - 'clicDvideo', 'clicGvideo', 'calculaudio', 'calculvideo', - 'phrasevideo', 'phraseaudio'] +######################################################################### +# We may want to map these indices to explicit condition names. +# For that, we define a list of 10 strings. +condition_ids = ['horizontal checkerboard', + 'vertical checkerboard', + 'right button press, auditory instructions', + 'left button press, auditory instructions', + 'right button press, visual instructions', + 'left button press, visual instructions', + 'mental computation, auditory instructions', + 'mental computation, visual instructions', + 'visual sentence', + 'auditory sentence'] trial_type = np.array([condition_ids[i] for i in trial_idx]) -events = pd.DataFrame({'trial_type': trial_type, 'onset': time}) + +######################################################################### +# We also define a duration (required by BIDS conventions) +duration = np.ones_like(onset) + + +######################################################################### +# Form an event dataframe from these information +import pandas as pd +events = pd.DataFrame({'trial_type': trial_type, + 'onset': onset, + 'duration': duration}) + +######################################################################### +# Export them to a csv file csvfile = 'localizer_paradigm.csv' events.to_csv(csvfile) - print("Created the paradigm file in %s " % csvfile) From 3999a722f153f0d85ddd22705f4c6b7af9820c62 Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Wed, 3 Oct 2018 18:43:27 +0200 Subject: [PATCH 110/210] Refactored datasets.fetch_spm_auditory(); It now creates its own events file - Added datasets._make_spm_auditory_events_file() - Renamed _glob_spm_auditory_data() to _prepare_downloaded_spm_auditory_data() and refactored it out. - Refactored datasets.fetch_spm_auditory() into 3 functions: _download_spm_auditory_data(), _prepare_downloaded_spm_auditory_data(), _make_spm_auditory_events_file() . --- nistats/datasets.py | 163 +++++++++++++++++++++++++++----------------- 1 file changed, 101 insertions(+), 62 deletions(-) diff --git a/nistats/datasets.py b/nistats/datasets.py index 448814af..ee0ba7c0 100644 --- a/nistats/datasets.py +++ b/nistats/datasets.py @@ -11,6 +11,7 @@ from botocore.handlers import disable_signing import nibabel as nib +import numpy as np import pandas as pd from nilearn.datasets.utils import (_fetch_file, _fetch_files, @@ -342,6 +343,97 @@ def fetch_localizer_first_level(data_dir=None, verbose=1): return Bunch(**params) +def _download_spm_auditory_data(data_dir, subject_dir, subject_id): + print("Data absent, downloading...") + url = ("http://www.fil.ion.ucl.ac.uk/spm/download/data/MoAEpilot/" + "MoAEpilot.zip") + archive_path = os.path.join(subject_dir, os.path.basename(url)) + _fetch_file(url, subject_dir) + try: + _uncompress_file(archive_path) + except: + print("Archive corrupted, trying to download it again.") + return fetch_spm_auditory(data_dir=data_dir, data_name="", + subject_id=subject_id) + + +def _prepare_downloaded_spm_auditory_data(subject_dir): + """glob data from subject_dir. + + """ + subject_data = {} + for file_name in SPM_AUDITORY_DATA_FILES: + file_path = os.path.join(subject_dir, file_name) + if os.path.exists(file_path): + subject_data[file_name] = file_path + else: + print("%s missing from filelist!" % file_name) + return None + + _subject_data = {} + _subject_data["func"] = sorted( + [subject_data[x] for x in subject_data.keys() + if re.match("^fM00223_0\d\d\.img$", os.path.basename(x))]) + + # volumes for this dataset of shape (64, 64, 64, 1); let's fix this + for x in _subject_data["func"]: + vol = nib.load(x) + if len(vol.shape) == 4: + vol = nib.Nifti1Image(vol.get_data()[:, :, :, 0], + vol.affine) + nib.save(vol, x) + + _subject_data["anat"] = [subject_data[x] for x in subject_data.keys() + if re.match("^sM00223_002\.img$", + os.path.basename(x))][0] + + # ... same thing for anat + vol = nib.load(_subject_data["anat"]) + if len(vol.shape) == 4: + vol = nib.Nifti1Image(vol.get_data()[:, :, :, 0], + vol.affine) + nib.save(vol, _subject_data["anat"]) + + return Bunch(**_subject_data) + + +def _make_spm_auditory_events_file(events_file_location): + """ + Accepts path of a directory and creates the events file necessary + for the spm_auditory dataset. + The provided path is expected to be the same as the directory contaning spm_auditory fMRI data. + The created file's name follows the format _events.tsv . + + Parameters + ---------- + events_file_location: string + The path where the events file will be created; + expected to be the directory with fMRI data. + The name of the last directory in the path + is used to generate the name of the events file. + + Returns + ------- + events_filepath: string + The full path of the created events file. + + """ + events_filename = os.path.basename(events_file_location) + '_events' + '.tsv' + events_filepath = os.path.join(events_file_location, events_filename) + tr = 7. + slice_time_ref = 0. + n_scans = 96 + epoch_duration = 6 * tr # duration in seconds + conditions = ['rest', 'active'] * 8 + n_blocks = len(conditions) + duration = epoch_duration * np.ones(n_blocks) + onset = np.linspace(0, (n_blocks - 1) * epoch_duration, n_blocks) + events = pd.DataFrame( + {'onset': onset, 'duration': duration, 'trial_type': conditions}) + events.to_csv(events_filepath, sep='\t', index=False, columns=['onset', 'duration', 'trial_type']) + return events_filepath + + def fetch_spm_auditory(data_dir=None, data_name='spm_auditory', subject_id="sub001", verbose=1): """Function to fetch SPM auditory single-subject data. @@ -369,69 +461,16 @@ def fetch_spm_auditory(data_dir=None, data_name='spm_auditory', data_dir = _get_dataset_dir(data_name, data_dir=data_dir, verbose=verbose) subject_dir = os.path.join(data_dir, subject_id) - - def _glob_spm_auditory_data(): - """glob data from subject_dir. - - """ - - if not os.path.exists(subject_dir): - return None - - subject_data = {} - for file_name in SPM_AUDITORY_DATA_FILES: - file_path = os.path.join(subject_dir, file_name) - if os.path.exists(file_path): - subject_data[file_name] = file_path - else: - print("%s missing from filelist!" % file_name) - return None - - _subject_data = {} - _subject_data["func"] = sorted( - [subject_data[x] for x in subject_data.keys() - if re.match("^fM00223_0\d\d\.img$", os.path.basename(x))]) - - # volumes for this dataset of shape (64, 64, 64, 1); let's fix this - for x in _subject_data["func"]: - vol = nib.load(x) - if len(vol.shape) == 4: - vol = nib.Nifti1Image(vol.get_data()[:, :, :, 0], - vol.affine) - nib.save(vol, x) - - _subject_data["anat"] = [subject_data[x] for x in subject_data.keys() - if re.match("^sM00223_002\.img$", - os.path.basename(x))][0] - - # ... same thing for anat - vol = nib.load(_subject_data["anat"]) - if len(vol.shape) == 4: - vol = nib.Nifti1Image(vol.get_data()[:, :, :, 0], - vol.affine) - nib.save(vol, _subject_data["anat"]) - - return Bunch(**_subject_data) - - # maybe data_dir already contains the data ? - data = _glob_spm_auditory_data() - if data is not None: - return data - - # No. Download the data - print("Data absent, downloading...") - url = ("http://www.fil.ion.ucl.ac.uk/spm/download/data/MoAEpilot/" - "MoAEpilot.zip") - archive_path = os.path.join(subject_dir, os.path.basename(url)) - _fetch_file(url, subject_dir) + if not os.path.exists(subject_dir): + _download_spm_auditory_data(data_dir, subject_dir, subject_id) + spm_auditory_data = _prepare_downloaded_spm_auditory_data(subject_dir) try: - _uncompress_file(archive_path) - except: - print("Archive corrupted, trying to download it again.") - return fetch_spm_auditory(data_dir=data_dir, data_name="", - subject_id=subject_id) - - return _glob_spm_auditory_data() + spm_auditory_data['paradigm'] + except KeyError: + events_file_location = os.path.dirname(spm_auditory_data['func'][0]) + events_filepath = _make_spm_auditory_events_file(events_file_location) + spm_auditory_data['paradigm'] = events_filepath + return spm_auditory_data def fetch_spm_multimodal_fmri(data_dir=None, data_name="spm_multimodal_fmri", From df13582c76749b727eab40b0252298d1625d3e73 Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Wed, 3 Oct 2018 18:44:20 +0200 Subject: [PATCH 111/210] Added unit test for datasets._make_spm_auditory_events_file() in nistats/tests/ --- .../test_make_spm_auditory_events_file.py | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 nistats/tests/test_make_spm_auditory_events_file.py diff --git a/nistats/tests/test_make_spm_auditory_events_file.py b/nistats/tests/test_make_spm_auditory_events_file.py new file mode 100644 index 00000000..14099b06 --- /dev/null +++ b/nistats/tests/test_make_spm_auditory_events_file.py @@ -0,0 +1,47 @@ +import os + +from nose.tools import assert_equal +import pandas as pd + +from nistats.datasets import _make_spm_auditory_events_file + + +def create_expected_data(): + expected_filename = 'tests_events.tsv' + expected_events_data = { + 'onset': [factor * 42.0 for factor in range(0, 16)], + 'duration': [42.0] * 16, + 'trial_type': ['rest', 'active'] * 8, + } + expected_events_data = pd.DataFrame(expected_events_data ) + expected_events_data_string = expected_events_data.to_csv(sep='\t', index=0, columns=['onset', 'duration', 'trial_type']) + return expected_events_data_string, expected_filename + + +def create_actual_data(): + events_filepath = _make_spm_auditory_events_file(events_file_location= + os.getcwd() + ) + events_filename = os.path.basename(events_filepath) + with open(events_filepath , 'r') as actual_events_file_obj: + actual_events_data_string = actual_events_file_obj.read() + return actual_events_data_string, events_filename, events_filepath + + +def run_test(): + try: + expected_events_data_string, expected_filename = create_expected_data() + actual_events_data_string, actual_filename, events_filepath = create_actual_data() + assert_equal(actual_filename, expected_filename) + assert_equal(actual_events_data_string, expected_events_data_string) + finally: + os.remove(events_filepath) + +if __name__ == '__main__': + run_test() + + + + + + From b93152a4cdd27049ff00bd11b60b6454c6aa94d0 Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Wed, 3 Oct 2018 18:46:21 +0200 Subject: [PATCH 112/210] Modified example to work with BIDS events file for spm_auditory dataset --- .../plot_single_subject_single_run.py | 37 +++---------------- 1 file changed, 6 insertions(+), 31 deletions(-) diff --git a/examples/01_tutorials/plot_single_subject_single_run.py b/examples/01_tutorials/plot_single_subject_single_run.py index 21f7713d..2077db28 100644 --- a/examples/01_tutorials/plot_single_subject_single_run.py +++ b/examples/01_tutorials/plot_single_subject_single_run.py @@ -65,38 +65,13 @@ # ------------------------------------ # # We must now provide a description of the experiment, that is, define the -# timing of the auditory stimulation and rest periods. According to -# the documentation of the dataset, there were sixteen 42s-long blocks --- in -# which 6 scans were acquired --- alternating between rest and -# auditory stimulation, starting with rest. -# -# The following table provide all the relevant informations: -# - -""" -duration, onset, trial_type - 42 , 0 , rest - 42 , 42 , active - 42 , 84 , rest - 42 , 126 , active - 42 , 168 , rest - 42 , 210 , active - 42 , 252 , rest - 42 , 294 , active - 42 , 336 , rest - 42 , 378 , active - 42 , 420 , rest - 42 , 462 , active - 42 , 504 , rest - 42 , 546 , active - 42 , 588 , rest - 42 , 630 , active -""" - -############################################################################### -# We can read such a table from a spreadsheet file created with OpenOffice Calcor Office Excel, and saved under the *comma separated values* format (``.csv``). +# timing of the auditory stimulation and rest periods. This is typically +# provided in an events.tsv or events.csv file. The path of this file is +# provided in the dataset. +# Please note that events are sometimes called paradigms, though this +# is contrary to BIDS specification. import pandas as pd -events = pd.read_csv('auditory_block_paradigm.csv') +events = pd.read_table(subject_data['paradigm']) print(events) ############################################################################### From 7fb6d7faa676a4f9d8aceb82c260b1d67e9b3b5e Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Wed, 3 Oct 2018 20:48:43 +0200 Subject: [PATCH 113/210] Removed test of filename check, since it doesn't work in a CI environment --- nistats/tests/test_make_spm_auditory_events_file.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/nistats/tests/test_make_spm_auditory_events_file.py b/nistats/tests/test_make_spm_auditory_events_file.py index 14099b06..400a546e 100644 --- a/nistats/tests/test_make_spm_auditory_events_file.py +++ b/nistats/tests/test_make_spm_auditory_events_file.py @@ -15,7 +15,7 @@ def create_expected_data(): } expected_events_data = pd.DataFrame(expected_events_data ) expected_events_data_string = expected_events_data.to_csv(sep='\t', index=0, columns=['onset', 'duration', 'trial_type']) - return expected_events_data_string, expected_filename + return expected_events_data_string def create_actual_data(): @@ -25,14 +25,13 @@ def create_actual_data(): events_filename = os.path.basename(events_filepath) with open(events_filepath , 'r') as actual_events_file_obj: actual_events_data_string = actual_events_file_obj.read() - return actual_events_data_string, events_filename, events_filepath + return actual_events_data_string, events_filepath def run_test(): try: - expected_events_data_string, expected_filename = create_expected_data() - actual_events_data_string, actual_filename, events_filepath = create_actual_data() - assert_equal(actual_filename, expected_filename) + expected_events_data_string = create_expected_data() + actual_events_data_string, events_filepath = create_actual_data() assert_equal(actual_events_data_string, expected_events_data_string) finally: os.remove(events_filepath) From 2c6c249a91fbd6405d0bb6ecc46d844965ad0f73 Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Wed, 3 Oct 2018 23:19:52 +0200 Subject: [PATCH 114/210] Refactored events filepath creation - Added nistats.datasets._make_path_events_file_spm_auditory(). --- nistats/datasets.py | 60 ++++++++++++------- .../test_make_spm_auditory_events_file.py | 14 ++--- 2 files changed, 44 insertions(+), 30 deletions(-) diff --git a/nistats/datasets.py b/nistats/datasets.py index ee0ba7c0..aa99e2b2 100644 --- a/nistats/datasets.py +++ b/nistats/datasets.py @@ -358,8 +358,13 @@ def _download_spm_auditory_data(data_dir, subject_dir, subject_id): def _prepare_downloaded_spm_auditory_data(subject_dir): - """glob data from subject_dir. - + """ Uncompresses downloaded spm_auditory dataset and organizes + the data into apprpriate directories. + + Parameters + --------- + subject_dir: string + Path to subject's data directory. """ subject_data = {} for file_name in SPM_AUDITORY_DATA_FILES: @@ -396,33 +401,42 @@ def _prepare_downloaded_spm_auditory_data(subject_dir): return Bunch(**_subject_data) + +def _make_path_events_file_spm_auditory_events(spm_auditory_data): + """ + Accepts data for spm_auditory dataset as Bunch + and constructs the filepath for its events descriptor file. + Parameters + ---------- + spm_auditory_data: Bunch + + Returns + ------- + events_filepath: string + Full path to the events.tsv file for spm_auditory dataset. + """ + events_file_location = os.path.dirname(spm_auditory_data['func'][0]) + events_filename = os.path.basename(events_file_location) + '_events' + '.tsv' + events_filepath = os.path.join(events_file_location, events_filename) + return events_filepath -def _make_spm_auditory_events_file(events_file_location): + +def _make_events_file_spm_auditory(events_filepath): """ - Accepts path of a directory and creates the events file necessary - for the spm_auditory dataset. - The provided path is expected to be the same as the directory contaning spm_auditory fMRI data. - The created file's name follows the format _events.tsv . + Accepts destination filepath including filename and + creates the events.tsv file for the spm_auditory dataset. Parameters ---------- - events_file_location: string + events_filepath: string The path where the events file will be created; - expected to be the directory with fMRI data. - The name of the last directory in the path - is used to generate the name of the events file. Returns ------- - events_filepath: string - The full path of the created events file. + None """ - events_filename = os.path.basename(events_file_location) + '_events' + '.tsv' - events_filepath = os.path.join(events_file_location, events_filename) tr = 7. - slice_time_ref = 0. - n_scans = 96 epoch_duration = 6 * tr # duration in seconds conditions = ['rest', 'active'] * 8 n_blocks = len(conditions) @@ -430,9 +444,9 @@ def _make_spm_auditory_events_file(events_file_location): onset = np.linspace(0, (n_blocks - 1) * epoch_duration, n_blocks) events = pd.DataFrame( {'onset': onset, 'duration': duration, 'trial_type': conditions}) - events.to_csv(events_filepath, sep='\t', index=False, columns=['onset', 'duration', 'trial_type']) - return events_filepath - + events.to_csv(events_filepath, sep='\t', index=False, + columns=['onset', 'duration', 'trial_type']) + def fetch_spm_auditory(data_dir=None, data_name='spm_auditory', subject_id="sub001", verbose=1): @@ -467,8 +481,10 @@ def fetch_spm_auditory(data_dir=None, data_name='spm_auditory', try: spm_auditory_data['paradigm'] except KeyError: - events_file_location = os.path.dirname(spm_auditory_data['func'][0]) - events_filepath = _make_spm_auditory_events_file(events_file_location) + events_filepath = _make_path_events_file_spm_auditory_events( + spm_auditory_data) + if not os.path.isfile(events_filepath): + _make_events_file_spm_auditory(events_filepath) spm_auditory_data['paradigm'] = events_filepath return spm_auditory_data diff --git a/nistats/tests/test_make_spm_auditory_events_file.py b/nistats/tests/test_make_spm_auditory_events_file.py index 400a546e..4431bb5b 100644 --- a/nistats/tests/test_make_spm_auditory_events_file.py +++ b/nistats/tests/test_make_spm_auditory_events_file.py @@ -3,11 +3,10 @@ from nose.tools import assert_equal import pandas as pd -from nistats.datasets import _make_spm_auditory_events_file +from nistats.datasets import _make_events_file_spm_auditory def create_expected_data(): - expected_filename = 'tests_events.tsv' expected_events_data = { 'onset': [factor * 42.0 for factor in range(0, 16)], 'duration': [42.0] * 16, @@ -19,10 +18,8 @@ def create_expected_data(): def create_actual_data(): - events_filepath = _make_spm_auditory_events_file(events_file_location= - os.getcwd() - ) - events_filename = os.path.basename(events_filepath) + events_filepath = os.path.join(os.getcwd(), 'tests_events.tsv') + _make_events_file_spm_auditory(events_filepath=events_filepath) with open(events_filepath , 'r') as actual_events_file_obj: actual_events_data_string = actual_events_file_obj.read() return actual_events_data_string, events_filepath @@ -30,11 +27,12 @@ def create_actual_data(): def run_test(): try: - expected_events_data_string = create_expected_data() actual_events_data_string, events_filepath = create_actual_data() - assert_equal(actual_events_data_string, expected_events_data_string) finally: os.remove(events_filepath) + expected_events_data_string = create_expected_data() + assert_equal(actual_events_data_string, expected_events_data_string) + if __name__ == '__main__': run_test() From 68c223f018d76c45dbed8c09e2191f1a23e0424d Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Thu, 4 Oct 2018 06:10:21 +0200 Subject: [PATCH 115/210] Moved test_make_bids_compliant_locizer & test_make_spm_auditory to test_datasets Changed the tutorial text in example plot_single_subject_run.py - Removed mention of events.csv . - Removed mention of events == paradigms. --- .../plot_single_subject_single_run.py | 4 +- nistats/tests/test_datasets.py | 85 ++++++++++++++++++- ...ant_localizer_first_level_paradigm_file.py | 52 ------------ .../test_make_spm_auditory_events_file.py | 44 ---------- 4 files changed, 84 insertions(+), 101 deletions(-) delete mode 100644 nistats/tests/test_make_bids_compliant_localizer_first_level_paradigm_file.py delete mode 100644 nistats/tests/test_make_spm_auditory_events_file.py diff --git a/examples/01_tutorials/plot_single_subject_single_run.py b/examples/01_tutorials/plot_single_subject_single_run.py index 2077db28..12668eff 100644 --- a/examples/01_tutorials/plot_single_subject_single_run.py +++ b/examples/01_tutorials/plot_single_subject_single_run.py @@ -66,10 +66,8 @@ # # We must now provide a description of the experiment, that is, define the # timing of the auditory stimulation and rest periods. This is typically -# provided in an events.tsv or events.csv file. The path of this file is +# provided in an events.tsv file. The path of this file is # provided in the dataset. -# Please note that events are sometimes called paradigms, though this -# is contrary to BIDS specification. import pandas as pd events = pd.read_table(subject_data['paradigm']) print(events) diff --git a/nistats/tests/test_datasets.py b/nistats/tests/test_datasets.py index 9c50dd7e..baedaa1e 100644 --- a/nistats/tests/test_datasets.py +++ b/nistats/tests/test_datasets.py @@ -1,8 +1,7 @@ import os import json +from tempfile import NamedTemporaryFile import zipfile -import numpy as np -from nose.tools import assert_true, assert_false, assert_equal, assert_raises import nibabel from nilearn._utils.testing import (mock_request, wrap_chunk_read_, @@ -12,6 +11,9 @@ from nilearn.datasets.utils import _get_dataset_dir from nilearn._utils.compat import _basestring from nose import with_setup +from nose.tools import assert_true, assert_false, assert_equal, assert_raises +import numpy as np +import pandas as pd from nistats import datasets @@ -148,6 +150,51 @@ def test_fetch_openneuro_dataset(): assert_true(len(dl_files) == 9) +def test_make_bids_compliant_localizer_first_level_paradigm_file(): + def _input_data_for_test_file(): + file_data = [ + [0, 'calculvideo', 0.0], + [0, 'calculvideo', 2.400000095], + [0, 'damier_H', 8.699999809], + [0, 'clicDaudio', 11.39999961], + ] + return pd.DataFrame(file_data) + + def _expected_output_data_from_test_file(): + file_data = [ + ['calculvideo', 0.0], + ['calculvideo', 2.400000095], + ['damier_H', 8.699999809], + ['clicDaudio', 11.39999961], + ] + file_data = pd.DataFrame(file_data) + file_data.columns = ['trial_type', 'onset'] + return file_data + + def run_test(): + data_for_tests = _input_data_for_test_file() + expected_data_from_test_file = _expected_output_data_from_test_file() + with NamedTemporaryFile(mode='w', + dir=os.getcwd(), + suffix='.csv') as temp_csv_obj: + data_for_tests.to_csv(temp_csv_obj.name, + index=False, + header=False, + sep=' ', + ) + datasets._make_bids_compliant_localizer_first_level_paradigm_file( + temp_csv_obj.name + ) + data_from_test_file_post_mod = pd.read_csv(temp_csv_obj.name, + sep='\t') + assert_true(all( + expected_data_from_test_file == data_from_test_file_post_mod + ) + ) + + run_test() + + @with_setup(setup_mock, teardown_mock) def test_fetch_localizer(): dataset = datasets.fetch_localizer_first_level() @@ -155,6 +202,40 @@ def test_fetch_localizer(): assert_true(isinstance(dataset.epi_img, _basestring)) +def test_make_spm_auditory_events_file(): + def create_expected_data(): + expected_events_data = { + 'onset': [factor * 42.0 for factor in range(0, 16)], + 'duration': [42.0] * 16, + 'trial_type': ['rest', 'active'] * 8, + } + expected_events_data = pd.DataFrame(expected_events_data) + expected_events_data_string = expected_events_data.to_csv( + sep='\t', + index=0, + columns=['onset', 'duration', 'trial_type'], + ) + return expected_events_data_string + + def create_actual_data(): + events_filepath = os.path.join(os.getcwd(), 'tests_events.tsv') + datasets._make_events_file_spm_auditory( + events_filepath=events_filepath) + with open(events_filepath, 'r') as actual_events_file_obj: + actual_events_data_string = actual_events_file_obj.read() + return actual_events_data_string, events_filepath + + def run_test(): + try: + actual_events_data_string, events_filepath = create_actual_data() + finally: + os.remove(events_filepath) + expected_events_data_string = create_expected_data() + assert_equal(actual_events_data_string, expected_events_data_string) + + run_test() + + @with_setup(setup_mock, teardown_mock) @with_setup(tst.setup_tmpdata, tst.teardown_tmpdata) def test_fetch_spm_auditory(): diff --git a/nistats/tests/test_make_bids_compliant_localizer_first_level_paradigm_file.py b/nistats/tests/test_make_bids_compliant_localizer_first_level_paradigm_file.py deleted file mode 100644 index 6bb21b3d..00000000 --- a/nistats/tests/test_make_bids_compliant_localizer_first_level_paradigm_file.py +++ /dev/null @@ -1,52 +0,0 @@ -import os -from tempfile import NamedTemporaryFile - -from nose.tools import assert_true -import pandas as pd -from nistats.datasets import _make_bids_compliant_localizer_first_level_paradigm_file - -def _input_data_for_test_file(): - file_data = [ - [0, 'calculvideo', 0.0], - [0, 'calculvideo', 2.400000095], - [0, 'damier_H', 8.699999809], - [0, 'clicDaudio', 11.39999961], - ] - return pd.DataFrame(file_data) - - -def _expected_output_data_from_test_file(): - file_data = [ - ['calculvideo', 0.0], - ['calculvideo', 2.400000095], - ['damier_H', 8.699999809], - ['clicDaudio', 11.39999961], - ] - file_data = pd.DataFrame(file_data) - file_data.columns = ['trial_type', 'onset'] - return file_data - - -def run_test(): - data_for_tests = _input_data_for_test_file() - expected_data_from_test_file = _expected_output_data_from_test_file() - with NamedTemporaryFile(mode='w', - dir=os.getcwd(), - suffix='.csv') as temp_csv_obj: - data_for_tests.to_csv(temp_csv_obj.name, - index=False, - header=False, - sep=' ', - ) - _make_bids_compliant_localizer_first_level_paradigm_file( - temp_csv_obj.name - ) - data_from_test_file_post_mod = pd.read_csv(temp_csv_obj.name, sep='\t') - assert_true(all( - expected_data_from_test_file == data_from_test_file_post_mod - ) - ) - - -if __name__ == '__main__': - run_test() diff --git a/nistats/tests/test_make_spm_auditory_events_file.py b/nistats/tests/test_make_spm_auditory_events_file.py deleted file mode 100644 index 4431bb5b..00000000 --- a/nistats/tests/test_make_spm_auditory_events_file.py +++ /dev/null @@ -1,44 +0,0 @@ -import os - -from nose.tools import assert_equal -import pandas as pd - -from nistats.datasets import _make_events_file_spm_auditory - - -def create_expected_data(): - expected_events_data = { - 'onset': [factor * 42.0 for factor in range(0, 16)], - 'duration': [42.0] * 16, - 'trial_type': ['rest', 'active'] * 8, - } - expected_events_data = pd.DataFrame(expected_events_data ) - expected_events_data_string = expected_events_data.to_csv(sep='\t', index=0, columns=['onset', 'duration', 'trial_type']) - return expected_events_data_string - - -def create_actual_data(): - events_filepath = os.path.join(os.getcwd(), 'tests_events.tsv') - _make_events_file_spm_auditory(events_filepath=events_filepath) - with open(events_filepath , 'r') as actual_events_file_obj: - actual_events_data_string = actual_events_file_obj.read() - return actual_events_data_string, events_filepath - - -def run_test(): - try: - actual_events_data_string, events_filepath = create_actual_data() - finally: - os.remove(events_filepath) - expected_events_data_string = create_expected_data() - assert_equal(actual_events_data_string, expected_events_data_string) - - -if __name__ == '__main__': - run_test() - - - - - - From e8c85e56dc5e2ef32033591887319039f06c6baf Mon Sep 17 00:00:00 2001 From: bthirion Date: Wed, 5 Sep 2018 17:19:03 +0200 Subject: [PATCH 116/210] Added another second-level example --- .../plot_second_level_association_test.py | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 examples/03_second_level_models/plot_second_level_association_test.py diff --git a/examples/03_second_level_models/plot_second_level_association_test.py b/examples/03_second_level_models/plot_second_level_association_test.py new file mode 100644 index 00000000..f35e3c55 --- /dev/null +++ b/examples/03_second_level_models/plot_second_level_association_test.py @@ -0,0 +1,68 @@ +""" +Group analysis of a motor task from the Localizer dataset +========================================================= + +This example shows the results obtained in a group analysis using a more +complex contrast than a one- or two-sample t test. +We use the [left button press (auditory cue)] task from the Localizer +dataset and seek association between the contrast values and a variate +that measures the speed of pseudo-word reading. No confounding variate +is included in the model. + + +""" +# Author: Virgile Fritsch, Bertrand Thirion, 2014 -- 2018 +import numpy as np +import matplotlib.pyplot as plt +from nilearn import datasets, plotting + +############################################################################## +# Load Localizer contrast +n_samples = 94 +localizer_dataset = datasets.fetch_localizer_contrasts( + ['left button press (auditory cue)'], n_subjects=n_samples) + +# print basic information on the dataset +print('First contrast nifti image (3D) is located at: %s' % + localizer_dataset.cmaps[0]) + +tested_var = localizer_dataset.ext_vars['pseudo'] +# Quality check / Remove subjects with bad tested variate +mask_quality_check = np.where(tested_var != b'None')[0] +n_samples = mask_quality_check.size +contrast_map_filenames = [localizer_dataset.cmaps[i] + for i in mask_quality_check] +tested_var = tested_var[mask_quality_check].astype(float).reshape((-1, 1)) +print("Actual number of subjects after quality check: %d" % n_samples) + + +############################################################################ +# Estimate second level model +# --------------------------- +# We define the input maps and the design matrix for the second level model +# and fit it. +import pandas as pd +design_matrix = pd.DataFrame( + np.hstack((tested_var, np.ones_like(tested_var))), + columns=['fluency', 'intercept']) + +from nistats.second_level_model import SecondLevelModel +model = SecondLevelModel(smoothing_fwhm=5.0) +model.fit(contrast_map_filenames, design_matrix=design_matrix) + +########################################################################## +# To estimate the contrast is very simple. We can just provide the column +# name of the design matrix. +z_map = model.compute_contrast('fluency', output_type='z_score') + +########################################################################### +# We threshold the second level contrast at uncorrected p < 0.001 and plot +from nistats.thresholding import map_threshold +_, threshold = map_threshold(z_map, threshold=.05, height_control='fdr') + +plotting.plot_stat_map( + z_map, threshold=threshold, colorbar=True, + title='Group-level association between motor activity \n' + 'and reading fluency (fdr<0.05') + +plotting.show() From 8443b2fef827974d05fc15e6fed0292127451229 Mon Sep 17 00:00:00 2001 From: bthirion Date: Thu, 4 Oct 2018 10:00:57 +0200 Subject: [PATCH 117/210] Improved the example --- .../plot_second_level_association_test.py | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/examples/03_second_level_models/plot_second_level_association_test.py b/examples/03_second_level_models/plot_second_level_association_test.py index f35e3c55..f0f21449 100644 --- a/examples/03_second_level_models/plot_second_level_association_test.py +++ b/examples/03_second_level_models/plot_second_level_association_test.py @@ -1,6 +1,6 @@ """ -Group analysis of a motor task from the Localizer dataset -========================================================= +Example of generic design in second-level models +================================================ This example shows the results obtained in a group analysis using a more complex contrast than a one- or two-sample t test. @@ -12,22 +12,28 @@ """ # Author: Virgile Fritsch, Bertrand Thirion, 2014 -- 2018 -import numpy as np -import matplotlib.pyplot as plt -from nilearn import datasets, plotting + ############################################################################## # Load Localizer contrast +from nilearn import datasets n_samples = 94 localizer_dataset = datasets.fetch_localizer_contrasts( ['left button press (auditory cue)'], n_subjects=n_samples) +############################################################################## # print basic information on the dataset print('First contrast nifti image (3D) is located at: %s' % localizer_dataset.cmaps[0]) +############################################################################## +# Load the behavioral variable tested_var = localizer_dataset.ext_vars['pseudo'] +print(tested_var) + +############################################################################## # Quality check / Remove subjects with bad tested variate +import numpy as np mask_quality_check = np.where(tested_var != b'None')[0] n_samples = mask_quality_check.size contrast_map_filenames = [localizer_dataset.cmaps[i] @@ -35,7 +41,6 @@ tested_var = tested_var[mask_quality_check].astype(float).reshape((-1, 1)) print("Actual number of subjects after quality check: %d" % n_samples) - ############################################################################ # Estimate second level model # --------------------------- @@ -46,6 +51,8 @@ np.hstack((tested_var, np.ones_like(tested_var))), columns=['fluency', 'intercept']) +########################################################################### +# Fit of the second-level model from nistats.second_level_model import SecondLevelModel model = SecondLevelModel(smoothing_fwhm=5.0) model.fit(contrast_map_filenames, design_matrix=design_matrix) @@ -56,10 +63,13 @@ z_map = model.compute_contrast('fluency', output_type='z_score') ########################################################################### -# We threshold the second level contrast at uncorrected p < 0.001 and plot +# We compute the fdr-corrected p = 0.05 threshold for these data from nistats.thresholding import map_threshold _, threshold = map_threshold(z_map, threshold=.05, height_control='fdr') +########################################################################### +#Let us plot the second level contrast at the computed thresholds +from nilearn import plotting plotting.plot_stat_map( z_map, threshold=threshold, colorbar=True, title='Group-level association between motor activity \n' From 37447648685b8d8836542b37e4e352e803c5b3ba Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Thu, 4 Oct 2018 11:36:43 +0200 Subject: [PATCH 118/210] Tweaked function names & docstrings --- nistats/datasets.py | 20 ++++++++++++++------ nistats/tests/test_datasets.py | 2 +- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/nistats/datasets.py b/nistats/datasets.py index aa99e2b2..bf44e2c7 100644 --- a/nistats/datasets.py +++ b/nistats/datasets.py @@ -18,6 +18,7 @@ _get_dataset_dir, _uncompress_file, ) +from scipy.io import loadmat from sklearn.datasets.base import Bunch from nistats.utils import _verify_events_file_uses_tab_separators @@ -362,9 +363,16 @@ def _prepare_downloaded_spm_auditory_data(subject_dir): the data into apprpriate directories. Parameters - --------- + ---------- subject_dir: string Path to subject's data directory. + + Returns + ------- + _subject_data: skl.Bunch object + Scikit-Learn Bunch object containing data of a single subject + from the SPM Auditory dataset. + """ subject_data = {} for file_name in SPM_AUDITORY_DATA_FILES: @@ -402,7 +410,7 @@ def _prepare_downloaded_spm_auditory_data(subject_dir): return Bunch(**_subject_data) -def _make_path_events_file_spm_auditory_events(spm_auditory_data): +def _make_path_events_file_spm_auditory_data(spm_auditory_data): """ Accepts data for spm_auditory dataset as Bunch and constructs the filepath for its events descriptor file. @@ -416,12 +424,12 @@ def _make_path_events_file_spm_auditory_events(spm_auditory_data): Full path to the events.tsv file for spm_auditory dataset. """ events_file_location = os.path.dirname(spm_auditory_data['func'][0]) - events_filename = os.path.basename(events_file_location) + '_events' + '.tsv' + events_filename = os.path.basename(events_file_location) + '_events.tsv' events_filepath = os.path.join(events_file_location, events_filename) return events_filepath -def _make_events_file_spm_auditory(events_filepath): +def _make_events_file_spm_auditory_data(events_filepath): """ Accepts destination filepath including filename and creates the events.tsv file for the spm_auditory dataset. @@ -481,10 +489,10 @@ def fetch_spm_auditory(data_dir=None, data_name='spm_auditory', try: spm_auditory_data['paradigm'] except KeyError: - events_filepath = _make_path_events_file_spm_auditory_events( + events_filepath = _make_path_events_file_spm_auditory_data( spm_auditory_data) if not os.path.isfile(events_filepath): - _make_events_file_spm_auditory(events_filepath) + _make_events_file_spm_auditory_data(events_filepath) spm_auditory_data['paradigm'] = events_filepath return spm_auditory_data diff --git a/nistats/tests/test_datasets.py b/nistats/tests/test_datasets.py index baedaa1e..e17a0fc5 100644 --- a/nistats/tests/test_datasets.py +++ b/nistats/tests/test_datasets.py @@ -219,7 +219,7 @@ def create_expected_data(): def create_actual_data(): events_filepath = os.path.join(os.getcwd(), 'tests_events.tsv') - datasets._make_events_file_spm_auditory( + datasets._make_events_file_spm_auditory_data( events_filepath=events_filepath) with open(events_filepath, 'r') as actual_events_file_obj: actual_events_data_string = actual_events_file_obj.read() From 172ebcd66dc0a6772021f3d17952b991cee5aa11 Mon Sep 17 00:00:00 2001 From: bthirion Date: Thu, 4 Oct 2018 12:30:27 +0200 Subject: [PATCH 119/210] Ad how fix of 4D image problem --- nistats/second_level_model.py | 11 ++++++++++- nistats/tests/test_second_level_model.py | 6 +++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/nistats/second_level_model.py b/nistats/second_level_model.py index 26816136..7993ffb4 100644 --- a/nistats/second_level_model.py +++ b/nistats/second_level_model.py @@ -19,6 +19,7 @@ from nilearn._utils.niimg_conversions import check_niimg from nilearn._utils import CacheMixin from nilearn.input_data import NiftiMasker +from nilearn.image import mean_img from patsy import DesignInfo from .first_level_model import FirstLevelModel @@ -209,6 +210,12 @@ def fit(self, second_level_input, confounds=None, design_matrix=None): if not isinstance(labels_dtype, np.object): raise ValueError('subject_label column must be of dtype ' 'object instead of dtype %s' % labels_dtype) + elif isinstance(second_level_input, (str, Nifti1Image)): + if design_matrix is None: + raise ValueError('List of niimgs as second_level_input' + ' require a design matrix to be provided') + second_level_input = check_niimg(niimg=second_level_input, + ensure_ndim=4) else: raise ValueError('second_level_input must be a list of' ' `FirstLevelModel` objects, a pandas DataFrame' @@ -262,6 +269,8 @@ def fit(self, second_level_input, confounds=None, design_matrix=None): sample_map = second_level_input['effects_map_path'][0] labels = second_level_input['subject_label'] subjects_label = labels.values.tolist() + elif isinstance(second_level_input, Nifti1Image): + sample_map = mean_img(second_level_input) elif isinstance(second_level_input[0], FirstLevelModel): sample_model = second_level_input[0] sample_condition = sample_model.design_matrices_[0].columns[0] @@ -271,7 +280,7 @@ def fit(self, second_level_input, confounds=None, design_matrix=None): subjects_label = labels else: # In this case design matrix had to be provided - sample_map = second_level_input[0] + sample_map = mean_img(second_level_input) # Create and set design matrix, if not given if design_matrix is None: diff --git a/nistats/tests/test_second_level_model.py b/nistats/tests/test_second_level_model.py index e3b9151f..48841404 100644 --- a/nistats/tests/test_second_level_model.py +++ b/nistats/tests/test_second_level_model.py @@ -18,7 +18,7 @@ from numpy.testing import (assert_almost_equal, assert_array_equal) from nibabel.tmpdirs import InTemporaryDirectory import pandas as pd - +from nilearn.image import concat_imgs # This directory path BASEDIR = os.path.dirname(os.path.abspath(__file__)) @@ -92,6 +92,7 @@ def test_fmri_inputs(): ['03', 'a', FUNCFILE]] niidf = pd.DataFrame(dfrows, columns=dfcols) niimgs = [FUNCFILE, FUNCFILE, FUNCFILE] + niimg_4d = concat_imgs(niimgs) confounds = pd.DataFrame([['01', 1], ['02', 2], ['03', 3]], columns=['subject_label', 'conf1']) sdes = pd.DataFrame(X[:3, :3], columns=['intercept', 'b', 'c']) @@ -110,6 +111,9 @@ def test_fmri_inputs(): SecondLevelModel().fit(niidf, None, sdes) # niimgs as input SecondLevelModel().fit(niimgs, None, sdes) + # 4d niimg as input + SecondLevelModel().fit(niimg_4d, None, sdes) + # test wrong input errors # test first level model requirements assert_raises(ValueError, SecondLevelModel().fit, flm) From 0e26092471c6d733c4224ffc34f386e19d6f79ee Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Thu, 4 Oct 2018 16:15:08 +0200 Subject: [PATCH 120/210] Refactored datasets.fetch_spm_multimodal_fmri() - datasets._get_func_data_spm_multimodal() - datasets._get_session_trials_spm_multimodal() - datasets._get_anatomical_data_spm_multimodal() - datasets._glob_spm_multimodal_fmri_data() - datasets._download_data_spm_multimodal() - Added datasets._make_events_file_spm_multimodal_fmri() [INCOMPLETE] --- nistats/datasets.py | 166 +++++++++++++++++++++++++++----------------- 1 file changed, 102 insertions(+), 64 deletions(-) diff --git a/nistats/datasets.py b/nistats/datasets.py index bf44e2c7..b6e25d47 100644 --- a/nistats/datasets.py +++ b/nistats/datasets.py @@ -497,6 +497,105 @@ def fetch_spm_auditory(data_dir=None, data_name='spm_auditory', return spm_auditory_data +def _get_func_data_spm_multimodal(subject_dir, session, _subject_data): + session_func = sorted(glob.glob( + os.path.join( + subject_dir, + ("fMRI/Session%i/fMETHODS-000%i-*-01.img" % ( + session + 1, session + 5))))) + if len(session_func) < 390: + print("Missing %i functional scans for session %i." % ( + 390 - len(session_func), session)) + return None + + _subject_data['func%i' % (session + 1)] = session_func + return _subject_data + + +def _get_session_trials_spm_multimodal(subject_dir, session, _subject_data): + sess_trials = os.path.join( + subject_dir, + "fMRI/trials_ses%i.mat" % (session + 1)) + if not os.path.isfile(sess_trials): + print("Missing session file: %s" % sess_trials) + return None + + _subject_data['trials_ses%i' % (session + 1)] = sess_trials + return _subject_data + + +def _get_anatomical_data_spm_multimodal(subject_dir, _subject_data): + anat = os.path.join(subject_dir, "sMRI/smri.img") + if not os.path.isfile(anat): + print("Missing structural image.") + return None + + _subject_data["anat"] = anat + return _subject_data + + +def _glob_spm_multimodal_fmri_data(subject_dir): + """glob data from subject_dir.""" + _subject_data = {'slice_order': 'descending'} + + for session in range(2): + # glob func data for session s + 1 + _subject_data = _get_func_data_spm_multimodal(subject_dir, session, _subject_data) + if not _subject_data: + return None + # glob trials .mat file + _subject_data = _get_session_trials_spm_multimodal(subject_dir, session, _subject_data) + if not _subject_data: + return None + + # glob for anat data + _subject_data = _get_anatomical_data_spm_multimodal(subject_dir, _subject_data) + if not _subject_data: + return None + + return Bunch(**_subject_data) + + +def _download_data_spm_multimodal(data_dir, subject_dir, subject_id): + print("Data absent, downloading...") + urls = [ + # fmri + ("http://www.fil.ion.ucl.ac.uk/spm/download/data/mmfaces/" + "multimodal_fmri.zip"), + + # structural + ("http://www.fil.ion.ucl.ac.uk/spm/download/data/mmfaces/" + "multimodal_smri.zip") + ] + + for url in urls: + archive_path = os.path.join(subject_dir, os.path.basename(url)) + _fetch_file(url, subject_dir) + try: + _uncompress_file(archive_path) + except: + print("Archive corrupted, trying to download it again.") + return fetch_spm_multimodal_fmri(data_dir=data_dir, + data_name="", + subject_id=subject_id) + + return _glob_spm_multimodal_fmri_data(subject_dir) + + +def _make_events_file_spm_multimodal_fmri(subject_data, fmri_img): + tr = 2. + for idx in range(len(fmri_img)): + timing = loadmat(getattr(subject_data, "trials_ses%i" % (idx + 1)), + squeeze_me=True, struct_as_record=False) + faces_onsets = timing['onsets'][0].ravel() + scrambled_onsets = timing['onsets'][1].ravel() + onsets = np.hstack((faces_onsets, scrambled_onsets)) + onsets *= tr # because onsets were reporting in 'scans' units + conditions = (['faces'] * len(faces_onsets) + + ['scrambled'] * len(scrambled_onsets)) + paradigm = pd.DataFrame({'trial_type': conditions, 'onset': onsets}) + + def fetch_spm_multimodal_fmri(data_dir=None, data_name="spm_multimodal_fmri", subject_id="sub001", verbose=1): """Fetcher for Multi-modal Face Dataset. @@ -525,77 +624,16 @@ def fetch_spm_multimodal_fmri(data_dir=None, data_name="spm_multimodal_fmri", """ - data_dir = _get_dataset_dir(data_name, data_dir=data_dir, - verbose=verbose) + data_dir = _get_dataset_dir(data_name, data_dir=data_dir, verbose=verbose) subject_dir = os.path.join(data_dir, subject_id) - def _glob_spm_multimodal_fmri_data(): - """glob data from subject_dir.""" - _subject_data = {'slice_order': 'descending'} - - for session in range(2): - # glob func data for session s + 1 - session_func = sorted(glob.glob( - os.path.join( - subject_dir, - ("fMRI/Session%i/fMETHODS-000%i-*-01.img" % ( - session + 1, session + 5))))) - if len(session_func) < 390: - print("Missing %i functional scans for session %i." % ( - 390 - len(session_func), session)) - return None - - _subject_data['func%i' % (session + 1)] = session_func - - # glob trials .mat file - sess_trials = os.path.join( - subject_dir, - "fMRI/trials_ses%i.mat" % (session + 1)) - if not os.path.isfile(sess_trials): - print("Missing session file: %s" % sess_trials) - return None - - _subject_data['trials_ses%i' % (session + 1)] = sess_trials - - # glob for anat data - anat = os.path.join(subject_dir, "sMRI/smri.img") - if not os.path.isfile(anat): - print("Missing structural image.") - return None - - _subject_data["anat"] = anat - - return Bunch(**_subject_data) - # maybe data_dir already contains the data ? - data = _glob_spm_multimodal_fmri_data() + data = _glob_spm_multimodal_fmri_data(subject_dir) if data is not None: return data # No. Download the data - print("Data absent, downloading...") - urls = [ - # fmri - ("http://www.fil.ion.ucl.ac.uk/spm/download/data/mmfaces/" - "multimodal_fmri.zip"), - - # structural - ("http://www.fil.ion.ucl.ac.uk/spm/download/data/mmfaces/" - "multimodal_smri.zip") - ] - - for url in urls: - archive_path = os.path.join(subject_dir, os.path.basename(url)) - _fetch_file(url, subject_dir) - try: - _uncompress_file(archive_path) - except: - print("Archive corrupted, trying to download it again.") - return fetch_spm_multimodal_fmri(data_dir=data_dir, - data_name="", - subject_id=subject_id) - - return _glob_spm_multimodal_fmri_data() + return _download_data_spm_multimodal(data_dir, subject_dir, subject_id) def fetch_fiac_first_level(data_dir=None, verbose=1): From 615d45d9e96e95d56e1b79a6d49cfb3fb944fe49 Mon Sep 17 00:00:00 2001 From: bthirion Date: Thu, 4 Oct 2018 13:49:31 +0200 Subject: [PATCH 121/210] added output_image argument to plot_design_matrix --- nistats/reporting.py | 16 +++++++++++----- nistats/tests/test_reporting.py | 13 ++++++++++--- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/nistats/reporting.py b/nistats/reporting.py index e9400cd3..f915b611 100644 --- a/nistats/reporting.py +++ b/nistats/reporting.py @@ -15,7 +15,6 @@ from scipy import ndimage import nilearn.plotting # overrides the backend on headless servers from nilearn.image.resampling import coord_transform -import matplotlib import matplotlib.pyplot as plt from patsy import DesignInfo @@ -268,7 +267,7 @@ def compare_niimgs(ref_imgs, src_imgs, masker, plot_hist=True, log=True, return corrs -def plot_design_matrix(design_matrix, rescale=True, ax=None): +def plot_design_matrix(design_matrix, rescale=True, ax=None, output_file=None): """Plot a design matrix provided as a DataFrame Parameters @@ -282,6 +281,11 @@ def plot_design_matrix(design_matrix, rescale=True, ax=None): ax : axis handle, optional Handle to axis onto which we will draw design matrix. + output_file: string or None, optional, + The name of an image file to export the plot to. Valid extensions + are .png, .pdf, .svg. If output_file is not None, the plot + is saved to a file, and the display is closed. + Returns ------- ax: axis handle @@ -292,14 +296,13 @@ def plot_design_matrix(design_matrix, rescale=True, ax=None): from nilearn.plotting import _set_mpl_backend # avoid unhappy pyflakes _set_mpl_backend - import matplotlib.pyplot as plt # normalize the values per column for better visualization _, X, names = check_design_matrix(design_matrix) if rescale: X = X / np.maximum(1.e-12, np.sqrt(np.sum(X ** 2, 0))) # pylint: disable=no-member if ax is None: - plt.figure() + display = plt.figure() ax = plt.subplot(1, 1, 1) ax.imshow(X, interpolation='nearest', aspect='auto') @@ -310,7 +313,10 @@ def plot_design_matrix(design_matrix, rescale=True, ax=None): ax.set_xticklabels(names, rotation=60, ha='right') plt.tight_layout() - + if output_file is not None: + display.savefig(output_file) + display.close() + ax = None return ax diff --git a/nistats/tests/test_reporting.py b/nistats/tests/test_reporting.py index 6132426d..89a5a8da 100644 --- a/nistats/tests/test_reporting.py +++ b/nistats/tests/test_reporting.py @@ -5,6 +5,7 @@ import numpy as np from numpy.testing import dec from nose.tools import assert_true +from nibabel.tmpdirs import InTemporaryDirectory # Set the backend to avoid having DISPLAY problems from nilearn.plotting import _set_mpl_backend @@ -24,11 +25,17 @@ def test_show_design_matrix(): # test that the show code indeed (formally) runs frame_times = np.linspace(0, 127 * 1., 128) - DM = make_design_matrix( + dmtx = make_design_matrix( frame_times, drift_model='polynomial', drift_order=3) - ax = plot_design_matrix(DM) + ax = plot_design_matrix(dmtx) assert (ax is not None) - + with InTemporaryDirectory(): + ax = plot_design_matrix(dmtx, output_file='dmtx.png') + assert os.path.exists('dmtx.png') + assert (ax is None) + plot_design_matrix(dmtx, output_file='dmtx.pdf') + assert os.path.exists('dmtx.pdf') + def test_local_max(): shape = (9, 10, 11) From 3232db9d1ca10e914df36365724736b67d30586a Mon Sep 17 00:00:00 2001 From: bthirion Date: Thu, 4 Oct 2018 15:05:23 +0200 Subject: [PATCH 122/210] Attempt to fix the plt error --- nistats/reporting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nistats/reporting.py b/nistats/reporting.py index f915b611..7423c43a 100644 --- a/nistats/reporting.py +++ b/nistats/reporting.py @@ -315,7 +315,7 @@ def plot_design_matrix(design_matrix, rescale=True, ax=None, output_file=None): plt.tight_layout() if output_file is not None: display.savefig(output_file) - display.close() + plt.close() ax = None return ax From bd967d7f271914ec3415984e2eb2caf15e74a77d Mon Sep 17 00:00:00 2001 From: bthirion Date: Thu, 4 Oct 2018 15:35:04 +0200 Subject: [PATCH 123/210] Added the feature to plot_contrast_matrix +fixes --- nistats/reporting.py | 32 ++++++++++++++++++++++---------- nistats/tests/test_reporting.py | 24 +++++++++++++++++++++--- 2 files changed, 43 insertions(+), 13 deletions(-) diff --git a/nistats/reporting.py b/nistats/reporting.py index 7423c43a..4613a28f 100644 --- a/nistats/reporting.py +++ b/nistats/reporting.py @@ -314,13 +314,14 @@ def plot_design_matrix(design_matrix, rescale=True, ax=None, output_file=None): plt.tight_layout() if output_file is not None: - display.savefig(output_file) + plt.savefig(output_file) plt.close() ax = None return ax -def plot_contrast_matrix(contrast_def, design_matrix, colorbar=False, ax=None): +def plot_contrast_matrix(contrast_def, design_matrix, colorbar=False, ax=None, + output_file=None): """Creates plot for contrast definition. Parameters @@ -344,6 +345,12 @@ def plot_contrast_matrix(contrast_def, design_matrix, colorbar=False, ax=None): ax: matplotlib Axes object, optional (default None) Directory where plotted figures will be stored. + + output_file: string or None, optional, + The name of an image file to export the plot to. Valid extensions + are .png, .pdf, .svg. If output_file is not None, the plot + is saved to a file, and the display is closed. + Returns ------- @@ -352,19 +359,20 @@ def plot_contrast_matrix(contrast_def, design_matrix, colorbar=False, ax=None): design_column_names = design_matrix.columns.tolist() if isinstance(contrast_def, str): - di = DesignInfo(design_column_names) - contrast_def = di.linear_constraint(contrast_def).coefs + design_info = DesignInfo(design_column_names) + contrast_def = design_info.linear_constraint(contrast_def).coefs + + maxval = np.max(np.abs(contrast_def)) + con_matrix = np.asmatrix(contrast_def) if ax is None: plt.figure(figsize=(8, 4)) ax = plt.gca() - maxval = np.max(np.abs(contrast_def)) + mat = ax.matshow(con_matrix, aspect='equal', + extent=[0, con_matrix.shape[1], 0, con_matrix.shape[0]], + cmap='gray', vmin=-maxval, vmax=maxval) - con_mx = np.asmatrix(contrast_def) - mat = ax.matshow(con_mx, aspect='equal', extent=[0, con_mx.shape[1], - 0, con_mx.shape[0]], cmap='gray', vmin=-maxval, - vmax=maxval) ax.set_label('conditions') ax.set_ylabel('') ax.set_yticklabels(['' for x in ax.get_yticklabels()]) @@ -372,11 +380,15 @@ def plot_contrast_matrix(contrast_def, design_matrix, colorbar=False, ax=None): # Shift ticks to be at 0.5, 1.5, etc ax.xaxis.set(ticks=np.arange(1.0, len(design_column_names) + 1.0), ticklabels=design_column_names) - ax.set_xticklabels(design_column_names, rotation=90, ha='right') + ax.set_xticklabels(design_column_names, rotation=60, ha='right') if colorbar: plt.colorbar(mat, fraction=0.025, pad=0.04) plt.tight_layout() + if output_file is not None: + plt.savefig(output_file) + plt.close() + ax = None return ax diff --git a/nistats/tests/test_reporting.py b/nistats/tests/test_reporting.py index 89a5a8da..de513d90 100644 --- a/nistats/tests/test_reporting.py +++ b/nistats/tests/test_reporting.py @@ -1,12 +1,14 @@ -from nistats.design_matrix import make_design_matrix -from nistats.reporting import (plot_design_matrix, get_clusters_table, - _local_max) +import os import nibabel as nib import numpy as np from numpy.testing import dec from nose.tools import assert_true from nibabel.tmpdirs import InTemporaryDirectory +from nistats.design_matrix import make_design_matrix +from nistats.reporting import (plot_design_matrix, get_clusters_table, + _local_max, plot_contrast_matrix) + # Set the backend to avoid having DISPLAY problems from nilearn.plotting import _set_mpl_backend # Avoid making pyflakes unhappy @@ -35,6 +37,22 @@ def test_show_design_matrix(): assert (ax is None) plot_design_matrix(dmtx, output_file='dmtx.pdf') assert os.path.exists('dmtx.pdf') + +@dec.skipif(not have_mpl) +def test_show_contrast_matrix(): + # test that the show code indeed (formally) runs + frame_times = np.linspace(0, 127 * 1., 128) + dmtx = make_design_matrix( + frame_times, drift_model='polynomial', drift_order=3) + contrast = np.ones(4) + ax = plot_contrast_matrix(contrast, dmtx) + assert (ax is not None) + with InTemporaryDirectory(): + ax = plot_contrast_matrix(contrast, dmtx, output_file='contrast.png') + assert os.path.exists('contrast.png') + assert (ax is None) + plot_contrast_matrix(contrast, dmtx, output_file='contrast.pdf') + assert os.path.exists('contrast.pdf') def test_local_max(): From 1edf459ba0a8c2e34421115e63b5109504c6bfed Mon Sep 17 00:00:00 2001 From: bthirion Date: Thu, 4 Oct 2018 22:34:59 +0200 Subject: [PATCH 124/210] removed unused diaply variable --- nistats/reporting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nistats/reporting.py b/nistats/reporting.py index 4613a28f..15bd4334 100644 --- a/nistats/reporting.py +++ b/nistats/reporting.py @@ -302,7 +302,7 @@ def plot_design_matrix(design_matrix, rescale=True, ax=None, output_file=None): if rescale: X = X / np.maximum(1.e-12, np.sqrt(np.sum(X ** 2, 0))) # pylint: disable=no-member if ax is None: - display = plt.figure() + plt.figure() ax = plt.subplot(1, 1, 1) ax.imshow(X, interpolation='nearest', aspect='auto') From 5a53caa67956eead11bc4c8b2d01bd32e0c94bcf Mon Sep 17 00:00:00 2001 From: bthirion Date: Thu, 4 Oct 2018 22:44:21 +0200 Subject: [PATCH 125/210] Added an example in the tutorial section --- .../plot_single_subject_single_run.py | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/examples/01_tutorials/plot_single_subject_single_run.py b/examples/01_tutorials/plot_single_subject_single_run.py index 21f7713d..c72d432a 100644 --- a/examples/01_tutorials/plot_single_subject_single_run.py +++ b/examples/01_tutorials/plot_single_subject_single_run.py @@ -143,6 +143,18 @@ plot_design_matrix(design_matrix) plt.show() +############################################################################### +# Save the design matrix image to disk +# first create a directory where you want to write the images + +import os +outdir = 'results' +if not os.path.exists(outdir): + os.mkdir(outdir) + +from os.path import join +plot_design_matrix(design_matrix, output_file=join(outdir, 'design_matrix.png')) + ############################################################################### # The first column contains the expected reponse profile of regions which are # sensitive to the auditory stimulation. @@ -240,19 +252,9 @@ plt.show() -############################################################################### -# We can save the effect and zscore maps to the disk -# first create a directory where you want to write the images - -import os -outdir = 'results' -if not os.path.exists(outdir): - os.mkdir(outdir) ############################################################################### -# Then save the images in this directory - -from os.path import join +# We can save the effect and zscore maps to the disk z_map.to_filename(join(outdir, 'active_vs_rest_z_map.nii.gz')) eff_map.to_filename(join(outdir, 'active_vs_rest_eff_map.nii.gz')) From 873bb7617d02b07357f184bad4360125144e209d Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Fri, 5 Oct 2018 00:41:10 +0200 Subject: [PATCH 126/210] datasets.fetch_spm_multimodal_fmri() generates events.tsv files; warns in tests - Added _make_events_filepath_spm_multimodal_fmri(). - Events files are not egnerated during nosetests, and a warning is presented. --- nistats/datasets.py | 41 ++++++++++++++++++++++++++++++----------- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/nistats/datasets.py b/nistats/datasets.py index b6e25d47..f07638c9 100644 --- a/nistats/datasets.py +++ b/nistats/datasets.py @@ -8,6 +8,7 @@ import json import os import re +import warnings from botocore.handlers import disable_signing import nibabel as nib @@ -19,6 +20,7 @@ _uncompress_file, ) from scipy.io import loadmat +from scipy.io.matlab.miobase import MatReadError from sklearn.datasets.base import Bunch from nistats.utils import _verify_events_file_uses_tab_separators @@ -547,6 +549,15 @@ def _glob_spm_multimodal_fmri_data(subject_dir): _subject_data = _get_session_trials_spm_multimodal(subject_dir, session, _subject_data) if not _subject_data: return None + try: + paradigm = _make_events_file_spm_multimodal_fmri(_subject_data, session) + except MatReadError as mat_err: + warnings.warn('{}. An events.tsv file cannot be generated'.format(str(mat_err))) + else: + events_filepath = _make_events_filepath_spm_multimodal_fmri(_subject_data, session) + paradigm.to_csv(events_filepath, sep='\t', index=False) + _subject_data['events{}'.format(session+1)] = events_filepath + # glob for anat data _subject_data = _get_anatomical_data_spm_multimodal(subject_dir, _subject_data) @@ -582,18 +593,26 @@ def _download_data_spm_multimodal(data_dir, subject_dir, subject_id): return _glob_spm_multimodal_fmri_data(subject_dir) -def _make_events_file_spm_multimodal_fmri(subject_data, fmri_img): +def _make_events_filepath_spm_multimodal_fmri(_subject_data, session): + key = 'trials_ses{}'.format(session+1) + events_file_location = os.path.dirname(_subject_data[key]) + events_filename = 'session{}_events.tsv'.format(session+1) + events_filepath = os.path.join(events_file_location, events_filename) + return events_filepath + + +def _make_events_file_spm_multimodal_fmri(_subject_data, session): tr = 2. - for idx in range(len(fmri_img)): - timing = loadmat(getattr(subject_data, "trials_ses%i" % (idx + 1)), - squeeze_me=True, struct_as_record=False) - faces_onsets = timing['onsets'][0].ravel() - scrambled_onsets = timing['onsets'][1].ravel() - onsets = np.hstack((faces_onsets, scrambled_onsets)) - onsets *= tr # because onsets were reporting in 'scans' units - conditions = (['faces'] * len(faces_onsets) + - ['scrambled'] * len(scrambled_onsets)) - paradigm = pd.DataFrame({'trial_type': conditions, 'onset': onsets}) + timing = loadmat(_subject_data["trials_ses%i" % (session + 1)], + squeeze_me=True, struct_as_record=False) + faces_onsets = timing['onsets'][0].ravel() + scrambled_onsets = timing['onsets'][1].ravel() + onsets = np.hstack((faces_onsets, scrambled_onsets)) + onsets *= tr # because onsets were reporting in 'scans' units + conditions = (['faces'] * len(faces_onsets) + + ['scrambled'] * len(scrambled_onsets)) + paradigm = pd.DataFrame({'trial_type': conditions, 'onset': onsets}) + return paradigm def fetch_spm_multimodal_fmri(data_dir=None, data_name="spm_multimodal_fmri", From 9727fec6530cebac9fa0e14011a67fad07a9d657 Mon Sep 17 00:00:00 2001 From: KamalakerDadi Date: Fri, 5 Oct 2018 11:31:14 +0200 Subject: [PATCH 127/210] MAINT: Anaconda install requirements --- doc/install_doc_component.html | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/doc/install_doc_component.html b/doc/install_doc_component.html index 7e24fa0a..2cdbe51d 100644 --- a/doc/install_doc_component.html +++ b/doc/install_doc_component.html @@ -40,16 +40,16 @@

First: download and install 64 bit Anaconda

We recommend that you install a complete scientific Python - distribution like 64 bit Anaconda - . Since it meets all the requirements of nistats, it will save - you time and trouble. You could also check PythonXY - as an alternative.

+ distribution like 64 bit + Anaconda . Since it meets all the requirements of nistats, + it will save you time and trouble. You could also check + PythonXY as an alternative.

Nistats requires a Python installation and the following dependencies: ipython, scikit-learn, matplotlib and nibabel.

@@ -71,12 +71,12 @@
  • First: download and install 64 bit Anaconda

    We recommend that you install a complete scientific Python distribution like 64 bit + href="https://www.anaconda.com/download/#macos" target="_blank"> Anaconda. Since it meets all the requirements of nistats, it will save you time and trouble.

    @@ -112,7 +112,7 @@

    If you do not have access to the package manager we recommend that you install a complete scientific Python distribution like 64 bit + href="https://www.anaconda.com/download/#linux" target="_blank"> Anaconda. Since it meets all the requirements of nistats, it will save you time and trouble.

    From 6addef0f7becceb9674d75b5a77ccebd1bddf1ad Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Fri, 5 Oct 2018 12:59:51 +0200 Subject: [PATCH 128/210] Removed events data generating code from example plot_spm_multimodal_faces.py - datasets.fetch_spm_multimodal_fmri() generates sessions from 1, not 0. --- .../plot_spm_multimodal_faces.py | 18 +++---------- nistats/datasets.py | 25 +++++++++++-------- 2 files changed, 18 insertions(+), 25 deletions(-) diff --git a/examples/02_first_level_models/plot_spm_multimodal_faces.py b/examples/02_first_level_models/plot_spm_multimodal_faces.py index 22e94f44..687652f6 100644 --- a/examples/02_first_level_models/plot_spm_multimodal_faces.py +++ b/examples/02_first_level_models/plot_spm_multimodal_faces.py @@ -52,24 +52,14 @@ ######################################################################### # Make design matrices design_matrices = [] -for idx in range(len(fmri_img)): +for idx, img in enumerate(fmri_img, start=1): # Build paradigm - n_scans = fmri_img[idx].shape[-1] - timing = loadmat(getattr(subject_data, "trials_ses%i" % (idx + 1)), - squeeze_me=True, struct_as_record=False) - - faces_onsets = timing['onsets'][0].ravel() - scrambled_onsets = timing['onsets'][1].ravel() - onsets = np.hstack((faces_onsets, scrambled_onsets)) - onsets *= tr # because onsets were reporting in 'scans' units - conditions = (['faces'] * len(faces_onsets) + - ['scrambled'] * len(scrambled_onsets)) - paradigm = pd.DataFrame({'trial_type': conditions, 'onset': onsets}) - + n_scans = img.shape[-1] + events = pd.read_table(subject_data['events{}'.format(idx)]) # Build design matrix frame_times = np.arange(n_scans) * tr design_matrix = make_design_matrix( - frame_times, paradigm, hrf_model=hrf_model, drift_model=drift_model, + frame_times, events, hrf_model=hrf_model, drift_model=drift_model, period_cut=period_cut) design_matrices.append(design_matrix) diff --git a/nistats/datasets.py b/nistats/datasets.py index f07638c9..6bad47f0 100644 --- a/nistats/datasets.py +++ b/nistats/datasets.py @@ -504,25 +504,28 @@ def _get_func_data_spm_multimodal(subject_dir, session, _subject_data): os.path.join( subject_dir, ("fMRI/Session%i/fMETHODS-000%i-*-01.img" % ( - session + 1, session + 5))))) + session, session + 4) + ) + ) + )) if len(session_func) < 390: print("Missing %i functional scans for session %i." % ( 390 - len(session_func), session)) return None - _subject_data['func%i' % (session + 1)] = session_func + _subject_data['func%i' % (session)] = session_func return _subject_data def _get_session_trials_spm_multimodal(subject_dir, session, _subject_data): sess_trials = os.path.join( subject_dir, - "fMRI/trials_ses%i.mat" % (session + 1)) + "fMRI/trials_ses%i.mat" % (session)) if not os.path.isfile(sess_trials): print("Missing session file: %s" % sess_trials) return None - _subject_data['trials_ses%i' % (session + 1)] = sess_trials + _subject_data['trials_ses%i' % (session)] = sess_trials return _subject_data @@ -540,8 +543,8 @@ def _glob_spm_multimodal_fmri_data(subject_dir): """glob data from subject_dir.""" _subject_data = {'slice_order': 'descending'} - for session in range(2): - # glob func data for session s + 1 + for session in range(1, 3): + # glob func data for session _subject_data = _get_func_data_spm_multimodal(subject_dir, session, _subject_data) if not _subject_data: return None @@ -556,7 +559,7 @@ def _glob_spm_multimodal_fmri_data(subject_dir): else: events_filepath = _make_events_filepath_spm_multimodal_fmri(_subject_data, session) paradigm.to_csv(events_filepath, sep='\t', index=False) - _subject_data['events{}'.format(session+1)] = events_filepath + _subject_data['events{}'.format(session)] = events_filepath # glob for anat data @@ -594,16 +597,16 @@ def _download_data_spm_multimodal(data_dir, subject_dir, subject_id): def _make_events_filepath_spm_multimodal_fmri(_subject_data, session): - key = 'trials_ses{}'.format(session+1) + key = 'trials_ses{}'.format(session) events_file_location = os.path.dirname(_subject_data[key]) - events_filename = 'session{}_events.tsv'.format(session+1) + events_filename = 'session{}_events.tsv'.format(session) events_filepath = os.path.join(events_file_location, events_filename) return events_filepath def _make_events_file_spm_multimodal_fmri(_subject_data, session): tr = 2. - timing = loadmat(_subject_data["trials_ses%i" % (session + 1)], + timing = loadmat(_subject_data["trials_ses%i" % (session)], squeeze_me=True, struct_as_record=False) faces_onsets = timing['onsets'][0].ravel() scrambled_onsets = timing['onsets'][1].ravel() @@ -670,7 +673,7 @@ def _glob_fiac_data(): _subject_data = {} subject_dir = os.path.join(data_dir, 'nipy-data-0.2/data/fiac/fiac0') for session in [1, 2]: - # glob func data for session session + 1 + # glob func data for session session_func = os.path.join(subject_dir, 'run%i.nii.gz' % session) if not os.path.isfile(session_func): print('Missing functional scan for session %i.' % session) From e85d25152b34504e20ad6ed2f8f24914f0fef845 Mon Sep 17 00:00:00 2001 From: KamalakerDadi Date: Fri, 5 Oct 2018 13:17:08 +0200 Subject: [PATCH 129/210] DOC: Fix links in front end gallery --- doc/index.rst | 4 ++-- doc/themes/nistats/layout.html | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/index.rst b/doc/index.rst index fe363854..77c128d3 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -22,8 +22,8 @@ .. |first_level| image:: auto_examples/02_first_level_models/images/thumb/sphx_glr_plot_localizer_analysis_thumb.png :target: auto_examples/02_first_level_models/plot_localizer_analysis.html -.. |second_level| image:: auto_examples/03_second_level_models/images/thumb/sphx_glr_plot_second_level_button_press_thumb.png - :target: auto_examples/03_second_level_models/plot_second_level_button_press.html +.. |second_level| image:: auto_examples/03_second_level_models/images/thumb/sphx_glr_plot_thresholding_thumb.png + :target: auto_examples/03_second_level_models/plot_thresholding.html .. |bids| image:: auto_examples/01_tutorials/images/thumb/sphx_glr_plot_bids_analysis_thumb.png :target: auto_examples/01_tutorials/plot_bids_analysis.html diff --git a/doc/themes/nistats/layout.html b/doc/themes/nistats/layout.html index bcb354b8..cb4748c1 100644 --- a/doc/themes/nistats/layout.html +++ b/doc/themes/nistats/layout.html @@ -152,7 +152,7 @@ First Level
  • - Second Level + Second Level
  • BIDS datasets From 50a4908ac5b0103e8f91127e05541b8f8a6421c8 Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Fri, 5 Oct 2018 14:42:04 +0200 Subject: [PATCH 130/210] tsv and csv files are now correctly loaded as pandas dataframe - Added nistats.utils._read_events_table() --- nistats/utils.py | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/nistats/utils.py b/nistats/utils.py index a0d86b39..249bc65a 100644 --- a/nistats/utils.py +++ b/nistats/utils.py @@ -23,15 +23,39 @@ def _check_list_length_match(list_1, list_2, var_name_1, var_name_2): % (str(var_name_1), len(list_1), str(var_name_2), len(list_2))) +def _read_events_table(table): + """ + Accepts the path to en event.tsv file and loads it as a Pandas Dataframe. + Raises an error if loading fails. + Parameters + ---------- + table: string + Accepts the path to an events file + + Returns + ------- + loaded: pandas.Dataframe object + Pandas Dataframe witht e events data. + """ + try: + # kept for historical reasons, a lot of tests use csv with index column + loaded = pd.read_csv(table, index_col=0) + except: + raise ValueError('table path %s could not be loaded' % table) + if loaded.empty: + try: + loaded = pd.read_table(table) + except: + raise ValueError('table path %s could not be loaded' % table) + return loaded + + def _check_and_load_tables(tables_, var_name): """Check tables can be loaded in DataFrame to raise error if necessary""" tables = [] for table_idx, table in enumerate(tables_): if isinstance(table, _basestring): - try: - loaded = pd.read_csv(table, index_col=0) - except: - raise ValueError('table path %s could not be loaded' % table) + loaded = _read_events_table(table) tables.append(loaded) elif isinstance(table, pd.DataFrame): tables.append(table) From b1b859af8437282d897358dee561c6502bf0376f Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Fri, 5 Oct 2018 15:01:43 +0200 Subject: [PATCH 131/210] Example uses pd.read_table() for .tsv, as has correct default args; much cleaner --- .../02_first_level_models/plot_localizer_surface_analysis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/02_first_level_models/plot_localizer_surface_analysis.py b/examples/02_first_level_models/plot_localizer_surface_analysis.py index ba7fd046..f8a20e8c 100644 --- a/examples/02_first_level_models/plot_localizer_surface_analysis.py +++ b/examples/02_first_level_models/plot_localizer_surface_analysis.py @@ -29,7 +29,7 @@ from nistats.datasets import fetch_localizer_first_level data = fetch_localizer_first_level() paradigm_file = data.paradigm -paradigm = pd.read_csv(paradigm_file, sep='\t', index_col=None) +paradigm = pd.read_table(paradigm_file) fmri_img = data.epi_img ######################################################################### From 04977e4adf5374d48c6910d899296f4c87df5d5f Mon Sep 17 00:00:00 2001 From: bthirion Date: Fri, 5 Oct 2018 22:27:50 +0200 Subject: [PATCH 132/210] mix fixes --- .../plot_first_level_model_details.py | 10 +-- .../plot_single_subject_single_run.py | 82 +++++++++++++------ examples/03_second_level_models/plot_oasis.py | 4 +- .../plot_second_level_two_sample_test.py | 13 ++- .../plot_thresholding.py | 8 +- 5 files changed, 81 insertions(+), 36 deletions(-) diff --git a/examples/01_tutorials/plot_first_level_model_details.py b/examples/01_tutorials/plot_first_level_model_details.py index 1d777fc0..948f1b28 100644 --- a/examples/01_tutorials/plot_first_level_model_details.py +++ b/examples/01_tutorials/plot_first_level_model_details.py @@ -1,4 +1,4 @@ -"""Studying firts-level-model details in a trials-and-error fashion +"""Studying first-level-model details in a trials-and-error fashion ================================================================ In this tutorial, we study the parametrization of the first-level @@ -69,11 +69,11 @@ # Specification of the contrasts. # # For this, let's create a function that, given the design matrix, -# generates the corresponding contrasts. -# This will be useful to repeat contast specification when we change the design matrix. +# generates the corresponding contrasts. This will be useful to +# repeat contrast specification when we change the design matrix. def make_localizer_contrasts(design_matrix): - """ returns a dictionary of four contasts, given the design matrix""" + """ returns a dictionary of four contrasts, given the design matrix""" # first generate canonical contrasts contrast_matrix = np.eye(design_matrix.shape[1]) @@ -114,7 +114,7 @@ def make_localizer_contrasts(design_matrix): ######################################################################### # Contrast estimation and plotting # -# Since this script will be repeated several times, for the sake of readbility, +# Since this script will be repeated several times, for the sake of readability, # we encapsulate it in a function that we call when needed. # diff --git a/examples/01_tutorials/plot_single_subject_single_run.py b/examples/01_tutorials/plot_single_subject_single_run.py index de6e4b42..4ccf8d67 100644 --- a/examples/01_tutorials/plot_single_subject_single_run.py +++ b/examples/01_tutorials/plot_single_subject_single_run.py @@ -114,12 +114,12 @@ # # * t_r=7(s) is the time of repetition of acquisitions # * noise_model='ar1' specifies the noise covariance model: a lag-1 dependence -# * standardize=False means that we do not want to rescale the time series to mean 0, variance 1 -# * hrf_model='spm' means that we rely on the SPM "canonical hrf" model (without time or dispersion derivatives) +# * standardize=False means that we do not want to rescale the time +# series to mean 0, variance 1 +# * hrf_model='spm' means that we rely on the SPM "canonical hrf" +# model (without time or dispersion derivatives) # * drift_model='cosine' means that we model the signal drifts as slow oscillating time functions # * period_cut=160(s) defines the cutoff frequency (its inverse actually). -# - fmri_glm = FirstLevelModel(t_r=7, noise_model='ar1', standardize=False, @@ -159,8 +159,13 @@ # ----------------------------------------- # # To access the estimated coefficients (Betas of the GLM model), we -# created constrast with a single '1' in each of the columns: The role of the contrast is to select some columns of the model --and potentially weight them-- to study the associated statistics. So in a nutshell, a contrast is a weigted combination of the estimated effects. -# Here we can define canonical contrasts that just consider the two condition in isolation ---let's call them "conditions"--- then a contrast that makes the difference between these conditions. +# created constrast with a single '1' in each of the columns: The role +# of the contrast is to select some columns of the model --and +# potentially weight them-- to study the associated statistics. So in +# a nutshell, a contrast is a weigted combination of the estimated +# effects. Here we can define canonical contrasts that just consider +# the two condition in isolation ---let's call them "conditions"--- +# then a contrast that makes the difference between these conditions. from numpy import array conditions = { @@ -175,28 +180,37 @@ active_minus_rest = conditions['active'] - conditions['rest'] ############################################################################### -# Let's look at it: plot the coefficients of the contrast, indexed by the names of the columns of the design matrix. +# Let's look at it: plot the coefficients of the contrast, indexed by +# the names of the columns of the design matrix. from nistats.reporting import plot_contrast_matrix plot_contrast_matrix(active_minus_rest, design_matrix=design_matrix) ############################################################################### -# Below, we compute the estimated effect. It is in BOLD signal unit, but has no statistical guarantees, because it does not take into account the associated variance. +# Below, we compute the estimated effect. It is in BOLD signal unit, +# but has no statistical guarantees, because it does not take into +# account the associated variance. eff_map = fmri_glm.compute_contrast(active_minus_rest, output_type='effect_size') ############################################################################### -# In order to get statistical significance, we form a t-statistic, and directly convert is into z-scale. The z-scale means that the values are scaled to match a standard Gaussian distribution (mean=0, variance=1), across voxels, if there were now effects in the data. +# In order to get statistical significance, we form a t-statistic, and +# directly convert is into z-scale. The z-scale means that the values +# are scaled to match a standard Gaussian distribution (mean=0, +# variance=1), across voxels, if there were now effects in the data. z_map = fmri_glm.compute_contrast(active_minus_rest, output_type='z_score') ############################################################################### -# Plot thresholded z scores map -# We display it on top of the average functional image of the series (could be the anatomical image of the subject). -# We use arbitrarily a threshold of 3.0 in z-scale. We'll see later how to use corrected thresholds. -# we show to display 3 axial views: display_mode='z', cut_coords=3 +# Plot thresholded z scores map. +# +# We display it on top of the average +# functional image of the series (could be the anatomical image of the +# subject). We use arbitrarily a threshold of 3.0 in z-scale. We'll +# see later how to use corrected thresholds. we show to display 3 +# axial views: display_mode='z', cut_coords=3 plot_stat_map(z_map, bg_img=mean_img, threshold=3.0, display_mode='z', cut_coords=3, black_bg=True, @@ -204,8 +218,13 @@ plt.show() ############################################################################### -# Statistical signifiance testing -# One should worry about the statistical validity of the procedure: here we used an arbitrary threshold of 3.0 but the threshold should provide some guarantees on the risk of false detections (aka type-1 errors in statistics). One first suggestion is to control the false positive rate (fpr) at a certain level, e.g. 0.001: this means that there is.1% chance of declaring active an inactive voxel. +# Statistical signifiance testing. One should worry about the +# statistical validity of the procedure: here we used an arbitrary +# threshold of 3.0 but the threshold should provide some guarantees on +# the risk of false detections (aka type-1 errors in statistics). One +# first suggestion is to control the false positive rate (fpr) at a +# certain level, e.g. 0.001: this means that there is.1% chance of +# declaring active an inactive voxel. from nistats.thresholding import map_threshold _, threshold = map_threshold(z_map, threshold=.001, height_control='fpr') @@ -216,7 +235,11 @@ plt.show() ############################################################################### -# The problem is that with this you expect 0.001 * n_voxels to show up while they're not active --- tens to hundreds of voxels. A more conservative solution is to control the family wise errro rate, i.e. the probability of making ony one false detection, say at 5%. For that we use the so-called Bonferroni correction +# The problem is that with this you expect 0.001 * n_voxels to show up +# while they're not active --- tens to hundreds of voxels. A more +# conservative solution is to control the family wise errro rate, +# i.e. the probability of making ony one false detection, say at +# 5%. For that we use the so-called Bonferroni correction _, threshold = map_threshold(z_map, threshold=.05, height_control='bonferroni') print('Bonferroni-corrected, p<0.05 threshold: %.3f' % threshold) @@ -226,8 +249,10 @@ plt.show() ############################################################################### -# This is quite conservative indeed ! -# A popular alternative is to control the false discovery rate, i.e. the expected proportion of false discoveries among detections. This is called the false disovery rate +# This is quite conservative indeed ! A popular alternative is to +# control the false discovery rate, i.e. the expected proportion of +# false discoveries among detections. This is called the false +# disovery rate _, threshold = map_threshold(z_map, threshold=.05, height_control='fdr') print('False Discovery rate = 0.05 threshold: %.3f' % threshold) @@ -237,7 +262,11 @@ plt.show() ############################################################################### -# Finally people like to discard isolated voxels (aka "small clusters") from these images. It is possible to generate a thresholded map with small clusters removed by providing a cluster_threshold argument. here clusters smaller than 10 voxels will be discarded. +# Finally people like to discard isolated voxels (aka "small +# clusters") from these images. It is possible to generate a +# thresholded map with small clusters removed by providing a +# cluster_threshold argument. here clusters smaller than 10 voxels +# will be discarded. clean_map, threshold = map_threshold( z_map, threshold=.05, height_control='fdr', cluster_threshold=10) @@ -279,22 +308,29 @@ ############################################################################### # Performing an F-test # -# "active vs rest" is a typical t test: condition versus baseline. Another popular type of test is an F test in which one seeks whether a certain combination of conditions (possibly two-, three- or higher-dimensional) explains a significant proportion of the signal. -# Here one might for instance test which voxels are well explained by combination of the active and rest condition. +# "active vs rest" is a typical t test: condition versus +# baseline. Another popular type of test is an F test in which one +# seeks whether a certain combination of conditions (possibly two-, +# three- or higher-dimensional) explains a significant proportion of +# the signal. Here one might for instance test which voxels are well +# explained by combination of the active and rest condition. import numpy as np effects_of_interest = np.vstack((conditions['active'], conditions['rest'])) plot_contrast_matrix(effects_of_interest, design_matrix) plt.show() ############################################################################### -# Specify the contrast and compute the correspoding map. Actually, the contrast specification is done exactly the same way as for t contrasts. +# Specify the contrast and compute the correspoding map. Actually, the +# contrast specification is done exactly the same way as for t +# contrasts. z_map = fmri_glm.compute_contrast(effects_of_interest, output_type='z_score') plt.show() ############################################################################### -# Note that the statistic has been converted to a z-variable, which makes it easier to represent it. +# Note that the statistic has been converted to a z-variable, which +# makes it easier to represent it. clean_map, threshold = map_threshold( z_map, threshold=.05, height_control='fdr', cluster_threshold=10) diff --git a/examples/03_second_level_models/plot_oasis.py b/examples/03_second_level_models/plot_oasis.py index 964cc8a2..b08756cd 100644 --- a/examples/03_second_level_models/plot_oasis.py +++ b/examples/03_second_level_models/plot_oasis.py @@ -49,7 +49,7 @@ oasis_dataset.white_matter_maps[0]) # 3D data ############################################################################### -# Get a mask image: A mask of the cortex of the ISBM template +# Get a mask image: A mask of the cortex of the ICBM template gm_mask = datasets.fetch_icbm152_brain_gm_mask() ############################################################################### @@ -79,7 +79,7 @@ ########################################################################## # Specify and fit the second-level model when loading the data, we -# smooth a little bit tom improve statistical behavior +# smooth a little bit to improve statistical behavior from nistats.second_level_model import SecondLevelModel second_level_model = SecondLevelModel(smoothing_fwhm=2.0, mask=mask_img) diff --git a/examples/03_second_level_models/plot_second_level_two_sample_test.py b/examples/03_second_level_models/plot_second_level_two_sample_test.py index b9b3eba5..3e5eb391 100644 --- a/examples/03_second_level_models/plot_second_level_two_sample_test.py +++ b/examples/03_second_level_models/plot_second_level_two_sample_test.py @@ -1,5 +1,4 @@ -""" -Second-level fMRI model: a two-sample test +"""Second-level fMRI model: a two-sample test ========================================== Full step-by-step example of fitting a GLM to perform a second level analysis @@ -8,9 +7,15 @@ More specifically: 1. A sample of n=16 visual activity fMRIs are downloaded. -2. A two-sample t-test is applied to the brain maps in order to see the effect of the contrast difference across subjects. +2. A two-sample t-test is applied to the brain maps in order to see +the effect of the contrast difference across subjects. + +The contrast is between responses to vertical versus horizontal +checkerboards than are retinotopically distinct. At the individual +level, these stimuli are sometimes used to map the borders of primary +visual areas. At the group level, such a mapping is not possible. Yet, +we may observe some significant effects in these areas. -The contrast is between reponses to vertical versus horizontal checkerboards than are retinotopically distinct. At the individual level, these stimuli are sometimes used to map the borders of primary visual areas. At the group level, such a mapping is not possible. Yet, we may observe some significant effects in these areas. """ import pandas as pd diff --git a/examples/03_second_level_models/plot_thresholding.py b/examples/03_second_level_models/plot_thresholding.py index b5065b95..d8b6cc04 100644 --- a/examples/03_second_level_models/plot_thresholding.py +++ b/examples/03_second_level_models/plot_thresholding.py @@ -89,7 +89,11 @@ threshold=threshold3) ######################################################################### -# These different thresholds correpond to different statistical guarnatees: -# in the FWER corrected image there is only a probability<.05 of observing any false positive voxel. In the FDR-corrected image, 5% of the voxels found are likely to be false positive. In the uncorrected image, one expects a few tens of alse positive voxels. +# These different thresholds correspond to different statistical +# guarantees: in the FWER corrected image there is only a +# probability<.05 of observing any false positive voxel. In the +# FDR-corrected image, 5% of the voxels found are likely to be false +# positive. In the uncorrected image, one expects a few tens of false +# positive voxels. plotting.show() From a8f04a2211d87702bfa1b7c134c102740cad06a4 Mon Sep 17 00:00:00 2001 From: bthirion Date: Wed, 3 Oct 2018 22:41:19 +0200 Subject: [PATCH 133/210] changed the API and docstring of map_threshold --- .../plot_single_subject_single_run.py | 10 ++-- examples/03_second_level_models/plot_oasis.py | 7 +-- .../plot_thresholding.py | 4 +- nistats/tests/test_thresholding.py | 10 ++-- nistats/thresholding.py | 54 ++++++++++++++----- 5 files changed, 57 insertions(+), 28 deletions(-) diff --git a/examples/01_tutorials/plot_single_subject_single_run.py b/examples/01_tutorials/plot_single_subject_single_run.py index 4ccf8d67..e73531bb 100644 --- a/examples/01_tutorials/plot_single_subject_single_run.py +++ b/examples/01_tutorials/plot_single_subject_single_run.py @@ -227,7 +227,7 @@ # declaring active an inactive voxel. from nistats.thresholding import map_threshold -_, threshold = map_threshold(z_map, threshold=.001, height_control='fpr') +_, threshold = map_threshold(z_map, level=.001, height_control='fpr') print('Uncorrected p<0.001 threshold: %.3f' % threshold) plot_stat_map(z_map, bg_img=mean_img, threshold=threshold, display_mode='z', cut_coords=3, black_bg=True, @@ -241,7 +241,7 @@ # i.e. the probability of making ony one false detection, say at # 5%. For that we use the so-called Bonferroni correction -_, threshold = map_threshold(z_map, threshold=.05, height_control='bonferroni') +_, threshold = map_threshold(z_map, level=.05, height_control='bonferroni') print('Bonferroni-corrected, p<0.05 threshold: %.3f' % threshold) plot_stat_map(z_map, bg_img=mean_img, threshold=threshold, display_mode='z', cut_coords=3, black_bg=True, @@ -254,7 +254,7 @@ # false discoveries among detections. This is called the false # disovery rate -_, threshold = map_threshold(z_map, threshold=.05, height_control='fdr') +_, threshold = map_threshold(z_map, level=.05, height_control='fdr') print('False Discovery rate = 0.05 threshold: %.3f' % threshold) plot_stat_map(z_map, bg_img=mean_img, threshold=threshold, display_mode='z', cut_coords=3, black_bg=True, @@ -269,7 +269,7 @@ # will be discarded. clean_map, threshold = map_threshold( - z_map, threshold=.05, height_control='fdr', cluster_threshold=10) + z_map, level=.05, height_control='fdr', cluster_threshold=10) plot_stat_map(clean_map, bg_img=mean_img, threshold=threshold, display_mode='z', cut_coords=3, black_bg=True, title='Active minus Rest (fdr=0.05), clusters > 10 voxels') @@ -333,7 +333,7 @@ # makes it easier to represent it. clean_map, threshold = map_threshold( - z_map, threshold=.05, height_control='fdr', cluster_threshold=10) + z_map, level=.05, height_control='fdr', cluster_threshold=10) plot_stat_map(clean_map, bg_img=mean_img, threshold=threshold, display_mode='z', cut_coords=3, black_bg=True, title='Effects of interest (fdr=0.05), clusters > 10 voxels') diff --git a/examples/03_second_level_models/plot_oasis.py b/examples/03_second_level_models/plot_oasis.py index b08756cd..15c2b5ef 100644 --- a/examples/03_second_level_models/plot_oasis.py +++ b/examples/03_second_level_models/plot_oasis.py @@ -97,7 +97,7 @@ # First compute the threshold. from nistats.thresholding import map_threshold _, threshold = map_threshold( - z_map, threshold=.05, height_control='fdr') + z_map, level=.05, height_control='fdr') print('The FDR=.05-corrected threshold is: %.3g' % threshold) ########################################################################### @@ -116,11 +116,12 @@ z_map = second_level_model.compute_contrast(second_level_contrast='sex', output_type='z_score') _, threshold = map_threshold( - z_map, threshold=.05, height_control='fdr') + z_map, level=.05, height_control='fdr') plotting.plot_stat_map( z_map, threshold=threshold, colorbar=True, title='sex effect on grey matter density (FDR = .05)') ########################################################################### -# Note that there does not seem to be any significant effect of sex on grey matter density on that dataset. +# Note that there does not seem to be any significant effect of sex on +# grey matter density on that dataset. diff --git a/examples/03_second_level_models/plot_thresholding.py b/examples/03_second_level_models/plot_thresholding.py index d8b6cc04..5c30876e 100644 --- a/examples/03_second_level_models/plot_thresholding.py +++ b/examples/03_second_level_models/plot_thresholding.py @@ -47,12 +47,12 @@ # false positive rate < .001, cluster size > 10 voxels from nistats.thresholding import map_threshold thresholded_map1, threshold1 = map_threshold( - z_map, threshold=.001, height_control='fpr', cluster_threshold=10) + z_map, level=.001, height_control='fpr', cluster_threshold=10) ######################################################################### # Now use FDR <.05, (False Discovery Rate) no cluster-level threshold thresholded_map2, threshold2 = map_threshold( - z_map, threshold=.05, height_control='fdr') + z_map, level=.05, height_control='fdr') print('The FDR=.05 threshold is %.3g' % threshold2) ######################################################################### diff --git a/nistats/tests/test_thresholding.py b/nistats/tests/test_thresholding.py index 08e23a65..32cd7d52 100644 --- a/nistats/tests/test_thresholding.py +++ b/nistats/tests/test_thresholding.py @@ -2,7 +2,7 @@ """ import numpy as np from scipy.stats import norm -from nose.tools import assert_true +from nose.tools import assert_true, assert_raises from numpy.testing import assert_almost_equal, assert_equal import nibabel as nib from nistats.thresholding import fdr_threshold, map_threshold @@ -16,20 +16,22 @@ def test_fdr(): np.random.shuffle(x) assert_almost_equal(fdr_threshold(x, .1), norm.isf(.0005)) assert_true(fdr_threshold(x, .001) == np.infty) + assert_raises(ValueError, fdr_threshold, x, -.1) + assert_raises(ValueError, fdr_threshold, x, 1.5) def test_map_threshold(): shape = (9, 10, 11) p = np.prod(shape) data = norm.isf(np.linspace(1. / p, 1. - 1. / p, p)).reshape(shape) - threshold = .001 + alpha = .001 data[2:4, 5:7, 6:8] = 5. stat_img = nib.Nifti1Image(data, np.eye(4)) mask_img = nib.Nifti1Image(np.ones(shape), np.eye(4)) # test 1 th_map, _ = map_threshold( - stat_img, mask_img, threshold, height_control='fpr', + stat_img, mask_img, alpha, height_control='fpr', cluster_threshold=0) vals = th_map.get_data() assert_equal(np.sum(vals > 0), 8) @@ -43,7 +45,7 @@ def test_map_threshold(): # test 3:excessive size threshold th_map, z_th = map_threshold( - stat_img, mask_img, threshold, height_control='fpr', + stat_img, mask_img, alpha, height_control='fpr', cluster_threshold=10) vals = th_map.get_data() assert_true(np.sum(vals > 0) == 0) diff --git a/nistats/thresholding.py b/nistats/thresholding.py index 3cbc97f3..a5d1f573 100644 --- a/nistats/thresholding.py +++ b/nistats/thresholding.py @@ -10,21 +10,36 @@ def fdr_threshold(z_vals, alpha): - """ return the BH fdr for the input z_vals""" + """ return the Benjamini-Hochberg FDR threshold for the input z_vals + + Parameters + ---------- + z_vals: array, + a set of z-variates from which an FDR + alpha: float, + desired FDR control + + Returns + ------- + threshold: float, + FDR-controling threshold from the Benjamini-Hochberg procedure + """ + if alpha < 0 or alpha > 1: + raise ValueError('alpha should be between 0 and 1') z_vals_ = - np.sort(- z_vals) p_vals = norm.sf(z_vals_) n_samples = len(p_vals) pos = p_vals < alpha * np.linspace( .5 / n_samples, 1 - .5 / n_samples, n_samples) if pos.any(): - return (z_vals_[pos][-1] - 1.e-8) + return (z_vals_[pos][-1] - 1.e-12) else: return np.infty -def map_threshold(stat_img, mask_img=None, threshold=.001, +def map_threshold(stat_img, mask_img=None, level=.001, height_control='fpr', cluster_threshold=0): - """ Threshold the provided map + """ Compute the required threhsold level and return the thresholded map Parameters ---------- @@ -34,15 +49,21 @@ def map_threshold(stat_img, mask_img=None, threshold=.001, mask_img : Niimg-like object, optional, mask image - threshold: float, optional - cluster forming threshold (either a p-value or z-scale value) + level: float, optional + number controling the thresholding (either a p-value or z-scale value) + Not te be confused with the z-scale threshold: level can be a p-values, + e.g. "0.05" or another type of number depending on the height_ + control parameter. The z-scale threshold is actually returned by + the function. height_control: string, optional false positive control meaning of cluster forming threshold: 'fpr'|'fdr'|'bonferroni'|'none' cluster_threshold : float, optional - cluster size threshold + cluster size threshold. In the returned thresholded map, + sets of connected voxels (`clusters`) with size smaller + than this number will be removed. Returns ------- @@ -51,6 +72,11 @@ def map_threshold(stat_img, mask_img=None, threshold=.001, threshold: float, the voxel-level threshold used actually + + Note + ---- + If the input image is not z-scaled (i.e. some z-transformed statistic) + the computed threshold is not rigorous and likely meaningless """ # Masking if mask_img is None: @@ -62,24 +88,24 @@ def map_threshold(stat_img, mask_img=None, threshold=.001, # Thresholding if height_control == 'fpr': - z_th = norm.isf(threshold) + threshold = norm.isf(level) elif height_control == 'fdr': - z_th = fdr_threshold(stats, threshold) + threshold = fdr_threshold(stats, level) elif height_control == 'bonferroni': - z_th = norm.isf(threshold / n_voxels) + threshold = norm.isf(level / n_voxels) else: # Brute-force thresholding - z_th = threshold - stats *= (stats > z_th) + threshold = level + stats *= (stats > threshold) # embed it back to 3D grid stat_map = masker.inverse_transform(stats).get_data() # Extract connected components above threshold - label_map, n_labels = label(stat_map > z_th) + label_map, n_labels = label(stat_map > threshold) labels = label_map[masker.mask_img_.get_data() > 0] for label_ in range(1, n_labels + 1): if np.sum(labels == label_) < cluster_threshold: stats[labels == label_] = 0 - return masker.inverse_transform(stats), z_th + return masker.inverse_transform(stats), threshold From 598f1fab892b8336f83b5cba1b57e54b4c766407 Mon Sep 17 00:00:00 2001 From: bthirion Date: Thu, 4 Oct 2018 22:28:38 +0200 Subject: [PATCH 134/210] Further changes to threhsolding API --- doc/whats_new.rst | 5 ++-- nistats/tests/test_thresholding.py | 23 ++++++++++++++ nistats/thresholding.py | 48 +++++++++++++++++++++--------- 3 files changed, 60 insertions(+), 16 deletions(-) diff --git a/doc/whats_new.rst b/doc/whats_new.rst index 61943e9b..847cb4d7 100644 --- a/doc/whats_new.rst +++ b/doc/whats_new.rst @@ -1,4 +1,4 @@ -0.0.2a +0.0.1b ======= Changelog @@ -13,7 +13,8 @@ Updated the minimum versions of the dependencies * Pandas >= 0.18.0 * Sklearn >= 0.18.0 - +* third argument of map_threshold is now called 'level'. + 0.0.1a ======= diff --git a/nistats/tests/test_thresholding.py b/nistats/tests/test_thresholding.py index 32cd7d52..537d2690 100644 --- a/nistats/tests/test_thresholding.py +++ b/nistats/tests/test_thresholding.py @@ -72,3 +72,26 @@ def test_map_threshold(): cluster_threshold=0) vals = th_map.get_data() assert_equal(np.sum(vals > 0), 8) + + # test 7 without a map + th_map, threshold = map_threshold( + None, None, 3.0, height_control=None, + cluster_threshold=0) + assert_equal(threshold, 3.0) + assert_equal(th_map, None) + + th_map, threshold = map_threshold( + None, None, level=0.05, height_control='fpr', + cluster_threshold=0) + assert (threshold > 1.64) + assert_equal(th_map, None) + + assert_raises(ValueError, map_threshold, None, None, level=0.05, + height_control='fdr') + assert_raises(ValueError, map_threshold, None, None, level=0.05, + height_control='bonferroni') + + # test 8 wrong procedure + assert_raises(ValueError, map_threshold, None, None, level=0.05, + height_control='plop') + diff --git a/nistats/thresholding.py b/nistats/thresholding.py index a5d1f573..70005383 100644 --- a/nistats/thresholding.py +++ b/nistats/thresholding.py @@ -15,7 +15,7 @@ def fdr_threshold(z_vals, alpha): Parameters ---------- z_vals: array, - a set of z-variates from which an FDR + a set of z-variates from which the FDR is computed alpha: float, desired FDR control @@ -37,28 +37,31 @@ def fdr_threshold(z_vals, alpha): return np.infty -def map_threshold(stat_img, mask_img=None, level=.001, +def map_threshold(stat_img=None, mask_img=None, level=.001, height_control='fpr', cluster_threshold=0): - """ Compute the required threhsold level and return the thresholded map + """ Compute the required threshold level and return the thresholded map Parameters ---------- - stat_img : Niimg-like object, + stat_img : Niimg-like object or None, optional statistical image (presumably in z scale) + whenever height_control is 'fpr' or 'none', + stat_img=None os acceptable. + If it is 'fdr' or 'bonferroni', an error is raised mask_img : Niimg-like object, optional, mask image level: float, optional - number controling the thresholding (either a p-value or z-scale value) - Not te be confused with the z-scale threshold: level can be a p-values, + number controling the thresholding (either a p-value or z-scale value). + Not to be confused with the z-scale threshold: level can be a p-values, e.g. "0.05" or another type of number depending on the height_ control parameter. The z-scale threshold is actually returned by the function. - height_control: string, optional + height_control: string, or None optional false positive control meaning of cluster forming - threshold: 'fpr'|'fdr'|'bonferroni'|'none' + threshold: 'fpr'|'fdr'|'bonferroni'|None cluster_threshold : float, optional cluster size threshold. In the returned thresholded map, @@ -68,7 +71,7 @@ def map_threshold(stat_img, mask_img=None, level=.001, Returns ------- thresholded_map : Nifti1Image, - the stat_map theresholded at the prescribed voxel- and cluster-level + the stat_map thresholded at the prescribed voxel- and cluster-level threshold: float, the voxel-level threshold used actually @@ -78,6 +81,27 @@ def map_threshold(stat_img, mask_img=None, level=.001, If the input image is not z-scaled (i.e. some z-transformed statistic) the computed threshold is not rigorous and likely meaningless """ + # Check that height_control is correctly specified + if height_control not in ['fpr', 'fdr', 'bonferroni', None]: + raise ValueError( + "height control should be one of ['fpr', 'fdr', 'bonferroni', None]") + + # if height_control is 'fpr' or None, we don't need to look at the data + # to compute the threhsold + if height_control == 'fpr': + threshold = norm.isf(level) + elif height_control is None: + threshold = level + + # In this case, and is stat_img is None, we return + if stat_img is None: + if height_control in ['fpr', None]: + return None, threshold + else: + raise ValueError( + 'Map_threshold requires stat_img not to be None' + 'when the heigh_control procedure is bonferroni or fdr') + # Masking if mask_img is None: masker = NiftiMasker(mask_strategy='background').fit(stat_img) @@ -87,14 +111,10 @@ def map_threshold(stat_img, mask_img=None, level=.001, n_voxels = np.size(stats) # Thresholding - if height_control == 'fpr': - threshold = norm.isf(level) - elif height_control == 'fdr': + if height_control == 'fdr': threshold = fdr_threshold(stats, level) elif height_control == 'bonferroni': threshold = norm.isf(level / n_voxels) - else: # Brute-force thresholding - threshold = level stats *= (stats > threshold) # embed it back to 3D grid From 1448f52397675d004effb152ab167ef29f448dd2 Mon Sep 17 00:00:00 2001 From: bthirion Date: Fri, 5 Oct 2018 22:41:00 +0200 Subject: [PATCH 135/210] Some fixes in docstrings --- nistats/thresholding.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nistats/thresholding.py b/nistats/thresholding.py index 70005383..672aa474 100644 --- a/nistats/thresholding.py +++ b/nistats/thresholding.py @@ -45,9 +45,9 @@ def map_threshold(stat_img=None, mask_img=None, level=.001, ---------- stat_img : Niimg-like object or None, optional statistical image (presumably in z scale) - whenever height_control is 'fpr' or 'none', - stat_img=None os acceptable. - If it is 'fdr' or 'bonferroni', an error is raised + whenever height_control is 'fpr' or None, + stat_img=None is acceptable. + If it is 'fdr' or 'bonferroni', an error is raised if stat_img is None. mask_img : Niimg-like object, optional, mask image From 1e4ac9171579065942631627f8da1b942d2458f7 Mon Sep 17 00:00:00 2001 From: bthirion Date: Fri, 5 Oct 2018 23:28:57 +0200 Subject: [PATCH 136/210] last fix --- examples/03_second_level_models/plot_thresholding.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/examples/03_second_level_models/plot_thresholding.py b/examples/03_second_level_models/plot_thresholding.py index 5c30876e..5ee3446f 100644 --- a/examples/03_second_level_models/plot_thresholding.py +++ b/examples/03_second_level_models/plot_thresholding.py @@ -56,10 +56,11 @@ print('The FDR=.05 threshold is %.3g' % threshold2) ######################################################################### -# Now use FWER <.05, (Familywise Error Rate) no cluster-level threshold -# As the data have not been intensively smoothed, we can use a simple Bonferroni correction +# Now use FWER <.05, (Familywise Error Rate) no cluster-level +# threshold. As the data have not been intensively smoothed, we can +# use a simple Bonferroni correction thresholded_map3, threshold3 = map_threshold( - z_map, threshold=.05, height_control='bonferroni') + z_map, level=.05, height_control='bonferroni') print('The p<.05 Bonferroni-corrected threshold is %.3g' % threshold3) ######################################################################### From 6d9391d59a355adbd585283eaf68089aae604b67 Mon Sep 17 00:00:00 2001 From: bthirion Date: Sat, 6 Oct 2018 00:00:41 +0200 Subject: [PATCH 137/210] Further example fix --- .../plot_second_level_association_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/03_second_level_models/plot_second_level_association_test.py b/examples/03_second_level_models/plot_second_level_association_test.py index f0f21449..d862b437 100644 --- a/examples/03_second_level_models/plot_second_level_association_test.py +++ b/examples/03_second_level_models/plot_second_level_association_test.py @@ -65,7 +65,7 @@ ########################################################################### # We compute the fdr-corrected p = 0.05 threshold for these data from nistats.thresholding import map_threshold -_, threshold = map_threshold(z_map, threshold=.05, height_control='fdr') +_, threshold = map_threshold(z_map, level=.05, height_control='fdr') ########################################################################### #Let us plot the second level contrast at the computed thresholds From 84de0d4230d5bd269a2be7ebfb2102e33fcdc1bc Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Sat, 6 Oct 2018 11:11:01 +0200 Subject: [PATCH 138/210] Fixed error due to improper read of the new BIDS compliant events file - Replaced " with ' in non-comment stringss (PEP8). --- .../plot_first_level_model_details.py | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/examples/01_tutorials/plot_first_level_model_details.py b/examples/01_tutorials/plot_first_level_model_details.py index 948f1b28..1b22c0e3 100644 --- a/examples/01_tutorials/plot_first_level_model_details.py +++ b/examples/01_tutorials/plot_first_level_model_details.py @@ -43,8 +43,7 @@ # We just get the provided file and make it BIDS-compliant. t_r = 2.4 paradigm_file = data.paradigm -events= pd.read_csv(paradigm_file, sep=' ', header=None, index_col=None) -events.columns = ['session', 'trial_type', 'onset'] +events= pd.read_table(paradigm_file) ############################################################################### # Running a basic model @@ -81,21 +80,21 @@ def make_localizer_contrasts(design_matrix): for i, column in enumerate(design_matrix.columns)]) # Add more complex contrasts - contrasts["audio"] = contrasts["clicDaudio"] + contrasts["clicGaudio"] +\ - contrasts["calculaudio"] + contrasts["phraseaudio"] - contrasts["video"] = contrasts["clicDvideo"] + contrasts["clicGvideo"] + \ - contrasts["calculvideo"] + contrasts["phrasevideo"] - contrasts["computation"] = contrasts["calculaudio"] + contrasts["calculvideo"] - contrasts["sentences"] = contrasts["phraseaudio"] + contrasts["phrasevideo"] + contrasts['audio'] = contrasts['clicDaudio'] + contrasts['clicGaudio'] +\ + contrasts['calculaudio'] + contrasts['phraseaudio'] + contrasts['video'] = contrasts['clicDvideo'] + contrasts['clicGvideo'] + \ + contrasts['calculvideo'] + contrasts['phrasevideo'] + contrasts['computation'] = contrasts['calculaudio'] + contrasts['calculvideo'] + contrasts['sentences'] = contrasts['phraseaudio'] + contrasts['phrasevideo'] # Short dictionary of more relevant contrasts contrasts = { - "left-right": (contrasts["clicGaudio"] + contrasts["clicGvideo"] - - contrasts["clicDaudio"] - contrasts["clicDvideo"]), - "H-V": contrasts["damier_H"] - contrasts["damier_V"], - "audio-video": contrasts["audio"] - contrasts["video"], - "computation-sentences": (contrasts["computation"] - - contrasts["sentences"]), + 'left-right': (contrasts['clicGaudio'] + contrasts['clicGvideo'] + - contrasts['clicDaudio'] - contrasts['clicDvideo']), + 'H-V': contrasts['damier_H'] - contrasts['damier_V'], + 'audio-video': contrasts['audio'] - contrasts['video'], + 'computation-sentences': (contrasts['computation'] - + contrasts['sentences']), } return contrasts @@ -228,7 +227,7 @@ def plot_contrast(first_level_model): z_map = first_level_model.compute_contrast( contrast_val, output_type='z_score') plotting.plot_stat_map( - z_map, display_mode='z', threshold=3.0, title="effect of time derivatives") + z_map, display_mode='z', threshold=3.0, title='effect of time derivatives') plt.show() ######################################################################### From 752323105cf46d780df7102d7fb360220ba9953c Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Sun, 7 Oct 2018 16:22:40 +0200 Subject: [PATCH 139/210] Changed minimuum numpy ver to 1.11.0 for NiLearn compatibility - Added numpy=1.11.0 in TravisCI for py2, 3. - Removed the vestigial circleci_test_doc.sh --- .travis.yml | 4 ++-- continuous_integration/circle_ci_test_doc.sh | 10 ---------- nistats/version.py | 2 +- 3 files changed, 3 insertions(+), 13 deletions(-) delete mode 100644 continuous_integration/circle_ci_test_doc.sh diff --git a/.travis.yml b/.travis.yml index df81a511..974891fb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,7 +20,7 @@ matrix: include: # Ubuntu 14.04 version without matplotlib - env: DISTRIB="conda" PYTHON_VERSION="2.7" - NUMPY_VERSION="1.11.2" SCIPY_VERSION="0.17" + NUMPY_VERSION="1.11.0" SCIPY_VERSION="0.17" SCIKIT_LEARN_VERSION="0.18.0" PANDAS_VERSION="0.18.0" # Trying to get as close to the minimum required versions while @@ -31,7 +31,7 @@ matrix: NIBABEL_VERSION="2.0.2" PANDAS_VERSION="*" COVERAGE="true" # Python 3.4 with intermediary versions - env: DISTRIB="conda" PYTHON_VERSION="3.4" - NUMPY_VERSION="1.11.2" SCIPY_VERSION="0.17" + NUMPY_VERSION="1.11.0" SCIPY_VERSION="0.17" SCIKIT_LEARN_VERSION="0.18" MATPLOTLIB_VERSION="1.5.1" PANDAS_VERSION="0.18.0" PATSY_VERSION="*" # Python 3.5 with latest versions. diff --git a/continuous_integration/circle_ci_test_doc.sh b/continuous_integration/circle_ci_test_doc.sh deleted file mode 100644 index 5580d08e..00000000 --- a/continuous_integration/circle_ci_test_doc.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!bin/bash - -# on circle ci, each command run with its own execution context so we have to -# activate the conda testenv on a per command basis. That's why we put calls to -# python (conda) in a dedicated bash script and we activate the conda testenv -# here. -source activate testenv - -# pipefail is necessary to propagate exit codes -set -o pipefail && cd doc && make html 2>&1 | tee ~/log.txt diff --git a/nistats/version.py b/nistats/version.py index 22cd58fc..9a094a4d 100644 --- a/nistats/version.py +++ b/nistats/version.py @@ -30,7 +30,7 @@ # in some meaningful order (more => less 'core'). REQUIRED_MODULE_METADATA = ( ('numpy', { - 'min_version': '1.11.2', + 'min_version': '1.11', 'install_info': _NISTATS_INSTALL_MSG}), ('scipy', { 'min_version': '0.17', From 2e1dfc96553c4458a77036e178bead0e97efe7a3 Mon Sep 17 00:00:00 2001 From: rschmaelzle Date: Wed, 26 Sep 2018 21:45:14 -0400 Subject: [PATCH 140/210] Fix wrong paradigm key check --- nistats/experimental_paradigm.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nistats/experimental_paradigm.py b/nistats/experimental_paradigm.py index a2c29e60..f102dab2 100644 --- a/nistats/experimental_paradigm.py +++ b/nistats/experimental_paradigm.py @@ -55,12 +55,12 @@ def check_paradigm(paradigm): duration = np.zeros(n_events) modulation = np.ones(n_events) if 'trial_type' in paradigm.keys(): - warnings.warn("'trial_type' key not found in the given paradigm.") + warnings.warn("'trial_type' key found in the given paradigm.") trial_type = np.array(paradigm['trial_type']) if 'duration' in paradigm.keys(): - warnings.warn("'duration' key not found in the given paradigm.") + warnings.warn("'duration' key found in the given paradigm.") duration = np.array(paradigm['duration']).astype(np.float) if 'modulation' in paradigm.keys(): - warnings.warn("'modulation' key not found in the given paradigm.") + warnings.warn("'modulation' key found in the given paradigm.") modulation = np.array(paradigm['modulation']).astype(np.float) return trial_type, onset, duration, modulation From 06f2723d4f8ecf2de40804762e91e782c1ee251d Mon Sep 17 00:00:00 2001 From: rschmaelzle Date: Thu, 27 Sep 2018 21:26:38 -0400 Subject: [PATCH 141/210] Fix paradigm key checks for duration, trial_type, modulation --- nistats/experimental_paradigm.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/nistats/experimental_paradigm.py b/nistats/experimental_paradigm.py index f102dab2..abbe693a 100644 --- a/nistats/experimental_paradigm.py +++ b/nistats/experimental_paradigm.py @@ -48,18 +48,16 @@ def check_paradigm(paradigm): """ if 'onset' not in paradigm.keys(): raise ValueError('The provided paradigm has no onset key') + if 'duration' not in paradigm.keys(): + raise ValueError('The provided paradigm has no duration key') onset = np.array(paradigm['onset']) + duration = np.array(paradigm['duration']).astype(np.float) n_events = len(onset) trial_type = np.repeat('dummy', n_events) - duration = np.zeros(n_events) modulation = np.ones(n_events) - if 'trial_type' in paradigm.keys(): - warnings.warn("'trial_type' key found in the given paradigm.") - trial_type = np.array(paradigm['trial_type']) - if 'duration' in paradigm.keys(): - warnings.warn("'duration' key found in the given paradigm.") - duration = np.array(paradigm['duration']).astype(np.float) + if 'trial_type' not in paradigm.keys(): + warnings.warn("'trial_type' key not found in the given paradigm.") if 'modulation' in paradigm.keys(): warnings.warn("'modulation' key found in the given paradigm.") modulation = np.array(paradigm['modulation']).astype(np.float) From 2b061b2aebacea86be495b5d023800843fef916a Mon Sep 17 00:00:00 2001 From: rschmaelzle Date: Thu, 27 Sep 2018 21:40:06 -0400 Subject: [PATCH 142/210] correcting mistake re: trial type inclusion in paradigm check --- nistats/experimental_paradigm.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nistats/experimental_paradigm.py b/nistats/experimental_paradigm.py index abbe693a..fac895cb 100644 --- a/nistats/experimental_paradigm.py +++ b/nistats/experimental_paradigm.py @@ -54,10 +54,11 @@ def check_paradigm(paradigm): onset = np.array(paradigm['onset']) duration = np.array(paradigm['duration']).astype(np.float) n_events = len(onset) - trial_type = np.repeat('dummy', n_events) + trial_type = np.array(paradigm['trial_type']) modulation = np.ones(n_events) if 'trial_type' not in paradigm.keys(): warnings.warn("'trial_type' key not found in the given paradigm.") + trial_type = np.repeat('dummy', n_events) if 'modulation' in paradigm.keys(): warnings.warn("'modulation' key found in the given paradigm.") modulation = np.array(paradigm['modulation']).astype(np.float) From d568525ae680d10df5869f3dd0a5d6b8261e5932 Mon Sep 17 00:00:00 2001 From: rschmaelzle Date: Thu, 4 Oct 2018 09:50:41 -0400 Subject: [PATCH 143/210] add duration field to test-dataframes for paradigm & design matrix creation --- nistats/tests/test_dmtx.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/nistats/tests/test_dmtx.py b/nistats/tests/test_dmtx.py index 0e8defcf..e182dc41 100644 --- a/nistats/tests/test_dmtx.py +++ b/nistats/tests/test_dmtx.py @@ -45,8 +45,10 @@ def design_matrix_light( def basic_paradigm(): conditions = ['c0', 'c0', 'c0', 'c1', 'c1', 'c1', 'c2', 'c2', 'c2'] onsets = [30, 70, 100, 10, 30, 90, 30, 40, 60] + duration = 1 * np.ones(9) paradigm = pd.DataFrame({'trial_type': conditions, - 'onset': onsets}) + 'onset': onsets, + 'duration': duration}) return paradigm @@ -65,9 +67,11 @@ def modulated_block_paradigm(): def modulated_event_paradigm(): conditions = ['c0', 'c0', 'c0', 'c1', 'c1', 'c1', 'c2', 'c2', 'c2'] onsets = [30, 70, 100, 10, 30, 90, 30, 40, 60] + duration = 1 * np.ones(9) values = 1 + np.random.rand(len(onsets)) paradigm = pd.DataFrame({'trial_type': conditions, 'onset': onsets, + 'duration': duration, 'modulation': values}) return paradigm From c0f88052e8e19588e3e9c1904bd410af46497a90 Mon Sep 17 00:00:00 2001 From: rschmaelzle Date: Thu, 4 Oct 2018 10:20:39 -0400 Subject: [PATCH 144/210] renaming duration-> durations for clarity, and adding 'duration' to paradigm-test in various places --- nistats/tests/test_dmtx.py | 28 ++++++++++++++----------- nistats/tests/test_first_level_model.py | 4 +++- nistats/tests/test_paradigm.py | 15 ++++++++----- 3 files changed, 29 insertions(+), 18 deletions(-) diff --git a/nistats/tests/test_dmtx.py b/nistats/tests/test_dmtx.py index e182dc41..91e802f2 100644 --- a/nistats/tests/test_dmtx.py +++ b/nistats/tests/test_dmtx.py @@ -45,21 +45,21 @@ def design_matrix_light( def basic_paradigm(): conditions = ['c0', 'c0', 'c0', 'c1', 'c1', 'c1', 'c2', 'c2', 'c2'] onsets = [30, 70, 100, 10, 30, 90, 30, 40, 60] - duration = 1 * np.ones(9) + durations = 1 * np.ones(9) paradigm = pd.DataFrame({'trial_type': conditions, 'onset': onsets, - 'duration': duration}) + 'duration': durations}) return paradigm def modulated_block_paradigm(): conditions = ['c0', 'c0', 'c0', 'c1', 'c1', 'c1', 'c2', 'c2', 'c2'] onsets = [30, 70, 100, 10, 30, 90, 30, 40, 60] - duration = 5 + 5 * np.random.rand(len(onsets)) + durations = 5 + 5 * np.random.rand(len(onsets)) values = 1 + np.random.rand(len(onsets)) paradigm = pd.DataFrame({'trial_type': conditions, 'onset': onsets, - 'duration': duration, + 'duration': durations, 'modulation': values}) return paradigm @@ -67,11 +67,11 @@ def modulated_block_paradigm(): def modulated_event_paradigm(): conditions = ['c0', 'c0', 'c0', 'c1', 'c1', 'c1', 'c2', 'c2', 'c2'] onsets = [30, 70, 100, 10, 30, 90, 30, 40, 60] - duration = 1 * np.ones(9) + durations = 1 * np.ones(9) values = 1 + np.random.rand(len(onsets)) paradigm = pd.DataFrame({'trial_type': conditions, 'onset': onsets, - 'duration': duration, + 'duration': durations, 'modulation': values}) return paradigm @@ -79,10 +79,10 @@ def modulated_event_paradigm(): def block_paradigm(): conditions = ['c0', 'c0', 'c0', 'c1', 'c1', 'c1', 'c2', 'c2', 'c2'] onsets = [30, 70, 100, 10, 30, 90, 30, 40, 60] - duration = 5 * np.ones(9) + durations = 5 * np.ones(9) paradigm = pd.DataFrame({'trial_type': conditions, 'onset': onsets, - 'duration': duration}) + 'duration': durations}) return paradigm @@ -226,10 +226,12 @@ def test_design_matrix7(): tr = 1.0 frame_times = np.linspace(0, 127 * tr, 128) conditions = [0, 0, 0, 1, 1, 1, 3, 3, 3] + durations = 1 * np.ones(9) # no condition 'c2' onsets = [30, 70, 100, 10, 30, 90, 30, 40, 60] paradigm = pd.DataFrame({'trial_type': conditions, - 'onset': onsets}) + 'onset': onsets, + 'duration':durations}) hrf_model = 'glover' X, names = design_matrix_light(frame_times, paradigm, hrf_model=hrf_model, drift_model='polynomial', drift_order=3) @@ -473,8 +475,10 @@ def test_spm_1(): frame_times = np.linspace(0, 99, 100) conditions = ['c0', 'c0', 'c0', 'c1', 'c1', 'c1', 'c2', 'c2', 'c2'] onsets = [30, 50, 70, 10, 30, 80, 30, 40, 60] + durations = 1 * np.ones(9) paradigm = pd.DataFrame({'trial_type': conditions, - 'onset': onsets}) + 'onset': onsets, + 'duration': durations}) X1 = make_design_matrix(frame_times, paradigm, drift_model=None) _, matrix, _ = check_design_matrix(X1) spm_design_matrix = DESIGN_MATRIX['arr_0'] @@ -488,10 +492,10 @@ def test_spm_2(): frame_times = np.linspace(0, 99, 100) conditions = ['c0', 'c0', 'c0', 'c1', 'c1', 'c1', 'c2', 'c2', 'c2'] onsets = [30, 50, 70, 10, 30, 80, 30, 40, 60] - duration = 10 * np.ones(9) + durations = 10 * np.ones(9) paradigm = pd.DataFrame({'trial_type': conditions, 'onset': onsets, - 'duration': duration}) + 'duration': durations}) X1 = make_design_matrix(frame_times, paradigm, drift_model=None) spm_design_matrix = DESIGN_MATRIX['arr_1'] _, matrix, _ = check_design_matrix(X1) diff --git a/nistats/tests/test_first_level_model.py b/nistats/tests/test_first_level_model.py index 6e3b36ef..9c7996a0 100644 --- a/nistats/tests/test_first_level_model.py +++ b/nistats/tests/test_first_level_model.py @@ -217,8 +217,10 @@ def test_fmri_inputs(): def basic_paradigm(): conditions = ['c0', 'c0', 'c0', 'c1', 'c1', 'c1', 'c2', 'c2', 'c2'] onsets = [30, 70, 100, 10, 30, 90, 30, 40, 60] + durations = 1 * np.ones(9) paradigm = pd.DataFrame({'trial_type': conditions, - 'onset': onsets}) + 'onset': onsets, + 'duration': durations}) return paradigm diff --git a/nistats/tests/test_paradigm.py b/nistats/tests/test_paradigm.py index 37ae4a26..bdeb9bd4 100644 --- a/nistats/tests/test_paradigm.py +++ b/nistats/tests/test_paradigm.py @@ -15,18 +15,21 @@ def basic_paradigm(): conditions = ['c0', 'c0', 'c0', 'c1', 'c1', 'c1', 'c2', 'c2', 'c2'] onsets = [30, 70, 100, 10, 30, 90, 30, 40, 60] - paradigm = pd.DataFrame({'name': conditions, 'onset': onsets}) + durations = 1 * np.ones(9) + paradigm = pd.DataFrame({'name': conditions, + 'onset': onsets, + 'duration': durations}) return paradigm def modulated_block_paradigm(): conditions = ['c0', 'c0', 'c0', 'c1', 'c1', 'c1', 'c2', 'c2', 'c2'] onsets = [30, 70, 100, 10, 30, 90, 30, 40, 60] - duration = 5 + 5 * np.random.rand(len(onsets)) + durations = 5 + 5 * np.random.rand(len(onsets)) values = np.random.rand(len(onsets)) paradigm = pd.DataFrame({'name': conditions, 'onset': onsets, - 'duration': duration, + 'duration': durations, 'modulation': values}) return paradigm @@ -34,9 +37,11 @@ def modulated_block_paradigm(): def modulated_event_paradigm(): conditions = ['c0', 'c0', 'c0', 'c1', 'c1', 'c1', 'c2', 'c2', 'c2'] onsets = [30, 70, 100, 10, 30, 90, 30, 40, 60] + durations = 1 * np.ones(9) values = np.random.rand(len(onsets)) paradigm = pd.DataFrame({'name': conditions, 'onset': onsets, + 'durations': durations, 'amplitude': values}) return paradigm @@ -44,10 +49,10 @@ def modulated_event_paradigm(): def block_paradigm(): conditions = ['c0', 'c0', 'c0', 'c1', 'c1', 'c1', 'c2', 'c2', 'c2'] onsets = [30, 70, 100, 10, 30, 90, 30, 40, 60] - duration = 5 * np.ones(9) + durations = 5 * np.ones(9) paradigm = pd.DataFrame({'name': conditions, 'onset': onsets, - 'duration': duration}) + 'duration': durations}) return paradigm From 547e6a25b249b8c8d59a65946e1095d6dba84efe Mon Sep 17 00:00:00 2001 From: bthirion Date: Thu, 4 Oct 2018 23:12:05 +0200 Subject: [PATCH 145/210] started to fix the tests --- nistats/tests/test_dmtx.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/nistats/tests/test_dmtx.py b/nistats/tests/test_dmtx.py index 91e802f2..07799618 100644 --- a/nistats/tests/test_dmtx.py +++ b/nistats/tests/test_dmtx.py @@ -144,8 +144,9 @@ def test_convolve_regressors(): # tests for convolve_regressors helper function conditions = ['c0', 'c1'] onsets = [20, 40] - paradigm = pd.DataFrame({'trial_type': conditions, - 'onset': onsets}) + dduration = [1, 1] + paradigm = pd.DataFrame( + {'trial_type': conditions, 'onset': onsets, 'duration': duration}) # names not passed -> default names frame_times = np.arange(100) f, names = _convolve_regressors(paradigm, 'glover', frame_times) From 81e989558b354973dafdc61775cabdec4c643792 Mon Sep 17 00:00:00 2001 From: bthirion Date: Thu, 4 Oct 2018 23:25:02 +0200 Subject: [PATCH 146/210] Fixed typo --- nistats/tests/test_dmtx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nistats/tests/test_dmtx.py b/nistats/tests/test_dmtx.py index 07799618..8d775f03 100644 --- a/nistats/tests/test_dmtx.py +++ b/nistats/tests/test_dmtx.py @@ -144,7 +144,7 @@ def test_convolve_regressors(): # tests for convolve_regressors helper function conditions = ['c0', 'c1'] onsets = [20, 40] - dduration = [1, 1] + duration = [1, 1] paradigm = pd.DataFrame( {'trial_type': conditions, 'onset': onsets, 'duration': duration}) # names not passed -> default names From 15367ff76ccd2663503bd571b54c4430ef6be10b Mon Sep 17 00:00:00 2001 From: bthirion Date: Fri, 5 Oct 2018 10:44:32 +0200 Subject: [PATCH 147/210] Updated examples --- .../plot_localizer_analysis.py | 88 +++++++++++++++++++ .../plot_localizer_surface_analysis.py | 26 +++--- .../plot_spm_multimodal_faces.py | 19 ++-- .../plot_design_matrix.py | 7 +- 4 files changed, 121 insertions(+), 19 deletions(-) create mode 100644 examples/02_first_level_models/plot_localizer_analysis.py diff --git a/examples/02_first_level_models/plot_localizer_analysis.py b/examples/02_first_level_models/plot_localizer_analysis.py new file mode 100644 index 00000000..0ecca292 --- /dev/null +++ b/examples/02_first_level_models/plot_localizer_analysis.py @@ -0,0 +1,88 @@ +""" +First level analysis of localizer dataset +========================================= + +Full step-by-step example of fitting a GLM to experimental data and visualizing +the results. + +More specifically: + +1. A sequence of fMRI volumes are loaded +2. A design matrix describing all the effects related to the data is computed +3. a mask of the useful brain volume is computed +4. A GLM is applied to the dataset (effect/covariance, + then contrast estimation) + +""" +import numpy as np +import pandas as pd +from nilearn import plotting + +from nistats.first_level_model import FirstLevelModel +from nistats import datasets + +######################################################################### +# Prepare data and analysis parameters +# ------------------------------------- +# Prepare timing +t_r = 2.4 +slice_time_ref = 0.5 + +# Prepare data +data = datasets.fetch_localizer_first_level() +paradigm_file = data.paradigm +paradigm = pd.read_csv(paradigm_file, sep=' ', header=None, index_col=None) +paradigm.columns = ['session', 'trial_type', 'onset'] +paradigm['duration'] = np.ones_like(paradigm.onset) +fmri_img = data.epi_img + +######################################################################### +# Perform first level analysis +# ---------------------------- +# Setup and fit GLM +first_level_model = FirstLevelModel(t_r, slice_time_ref, + hrf_model='glover + derivative') +first_level_model = first_level_model.fit(fmri_img, paradigm) + +######################################################################### +# Estimate contrasts +# ------------------ +# Specify the contrasts +design_matrix = first_level_model.design_matrices_[0] +contrast_matrix = np.eye(design_matrix.shape[1]) +contrasts = dict([(column, contrast_matrix[i]) + for i, column in enumerate(design_matrix.columns)]) + +contrasts["audio"] = contrasts["clicDaudio"] + contrasts["clicGaudio"] +\ + contrasts["calculaudio"] + contrasts["phraseaudio"] +contrasts["video"] = contrasts["clicDvideo"] + contrasts["clicGvideo"] + \ + contrasts["calculvideo"] + contrasts["phrasevideo"] +contrasts["computation"] = contrasts["calculaudio"] + contrasts["calculvideo"] +contrasts["sentences"] = contrasts["phraseaudio"] + contrasts["phrasevideo"] + +######################################################################### +# Short list of more relevant contrasts +contrasts = { + "left-right": (contrasts["clicGaudio"] + contrasts["clicGvideo"] + - contrasts["clicDaudio"] - contrasts["clicDvideo"]), + "H-V": contrasts["damier_H"] - contrasts["damier_V"], + "audio-video": contrasts["audio"] - contrasts["video"], + "video-audio": -contrasts["audio"] + contrasts["video"], + "computation-sentences": (contrasts["computation"] - + contrasts["sentences"]), + "reading-visual": contrasts["phrasevideo"] - contrasts["damier_H"] + } + +######################################################################### +# contrast estimation +for index, (contrast_id, contrast_val) in enumerate(contrasts.items()): + print(' Contrast % 2i out of %i: %s' % + (index + 1, len(contrasts), contrast_id)) + z_map = first_level_model.compute_contrast(contrast_val, + output_type='z_score') + + # Create snapshots of the contrasts + display = plotting.plot_stat_map(z_map, display_mode='z', + threshold=3.0, title=contrast_id) + +plotting.show() diff --git a/examples/02_first_level_models/plot_localizer_surface_analysis.py b/examples/02_first_level_models/plot_localizer_surface_analysis.py index d27e9a83..9b987492 100644 --- a/examples/02_first_level_models/plot_localizer_surface_analysis.py +++ b/examples/02_first_level_models/plot_localizer_surface_analysis.py @@ -1,27 +1,33 @@ -""" -Example of surface-based first-level analysis +"""Example of surface-based first-level analysis ============================================= -Full step-by-step example of fitting a GLM to experimental data sampled on the cortical surface and visualizing the results. +Full step-by-step example of fitting a GLM to experimental data +sampled on the cortical surface and visualizing the results. More specifically: 1. A sequence of fMRI volumes are loaded -2. fMRI data are projected onto a reference cortical surface (the freesurfer template, fsaverage) +2. fMRI data are projected onto a reference cortical surface (the +freesurfer template, fsaverage) 3. A design matrix describing all the effects related to the data is computed 4. A GLM is applied to the dataset (effect/covariance, then contrast estimation) -The result of the analysis are statistical maps that are defined on the brain mesh. We disply them using Nilearn capabilities. - -The projection of fMRI data onto a given brain mesh requires that both are initially defined in the same space. +The result of the analysis are statistical maps that are defined on +the brain mesh. We disply them using Nilearn capabilities. -* The functional data should be coregistered to the anatomy from which the mesh was obtained. +The projection of fMRI data onto a given brain mesh requires that both +are initially defined in the same space. -* Another possibility, used here, is to project the normalized fMRI data to an MNI-coregistered mesh, such as fsaverage. +* The functional data should be coregistered to the anatomy from which + the mesh was obtained. -The advantage of this second approach is that it makes it easy to run second-level analyses on the surface. On the other hand, it is obviously less accurate than using a subject-tailored mesh. +* Another possibility, used here, is to project the normalized fMRI + data to an MNI-coregistered mesh, such as fsaverage. +The advantage of this second approach is that it makes it easy to run +second-level analyses on the surface. On the other hand, it is +obviously less accurate than using a subject-tailored mesh. """ diff --git a/examples/02_first_level_models/plot_spm_multimodal_faces.py b/examples/02_first_level_models/plot_spm_multimodal_faces.py index 5443e961..430032e8 100644 --- a/examples/02_first_level_models/plot_spm_multimodal_faces.py +++ b/examples/02_first_level_models/plot_spm_multimodal_faces.py @@ -54,7 +54,6 @@ ######################################################################### # Make design matrices import numpy as np -from scipy.io import loadmat import pandas as pd from nistats.design_matrix import make_design_matrix design_matrices = [] @@ -87,10 +86,12 @@ for i, column in enumerate(design_matrix.columns)]) ######################################################################### -# We actually want more interesting contrasts -# The simplest contrast just makes the difference between the two main conditions. -# We define the two opposite versions to run one-tail t-tests. -# We also define the effects of interest contrast, a 2-dimensional contrasts spanning the two conditions. +# We actually want more interesting contrasts. The simplest contrast +# just makes the difference between the two main conditions. We +# define the two opposite versions to run one-tail t-tests. We also +# define the effects of interest contrast, a 2-dimensional contrasts +# spanning the two conditions. + contrasts = { 'faces-scrambled': basic_contrasts['faces'] - basic_contrasts['scrambled'], 'scrambled-faces': -basic_contrasts['faces'] + basic_contrasts['scrambled'], @@ -125,7 +126,11 @@ cut_coords=3, black_bg=True, title=contrast_id) ######################################################################### -# Show the resulting maps: -# We observe that the analysis results in wide activity for the 'effects of interest' contrast, showing the implications of large portions of the visual cortex in the conditions. By contrast, the differential effect between "faces" and "scambled" involves sparser, more anterior and lateral regions. It displays also some responses in the frontal lobe. +# Show the resulting maps: We observe that the analysis results in +# wide activity for the 'effects of interest' contrast, showing the +# implications of large portions of the visual cortex in the +# conditions. By contrast, the differential effect between "faces" and +# "scambled" involves sparser, more anterior and lateral regions. It +# displays also some responses in the frontal lobe. plotting.show() diff --git a/examples/04_low_level_functions/plot_design_matrix.py b/examples/04_low_level_functions/plot_design_matrix.py index d7dc9088..4f87a22e 100644 --- a/examples/04_low_level_functions/plot_design_matrix.py +++ b/examples/04_low_level_functions/plot_design_matrix.py @@ -29,6 +29,7 @@ # these are the types of the different trials conditions = ['c0', 'c0', 'c0', 'c1', 'c1', 'c1', 'c3', 'c3', 'c3'] +duration = [1., 1., 1., 1., 1., 1., 1., 1., 1.] # these are the corresponding onset times onsets = [30., 70., 100., 10., 30., 90., 30., 40., 60.] # Next, we simulate 6 motion parameters jointly observed with fMRI acquisitions @@ -43,7 +44,8 @@ # The same parameters allow us to obtain a variety of design matrices # We first create an event object import pandas as pd -paradigm = pd.DataFrame({'trial_type': conditions, 'onset': onsets}) +paradigm = pd.DataFrame({'trial_type': conditions, 'onset': onsets, + 'duration': duration}) ######################################################################### # We sample the paradigm into a design matrix, also including additional regressors @@ -67,7 +69,8 @@ ######################################################################### # Finally we compute a FIR model -paradigm = pd.DataFrame({'trial_type': conditions, 'onset': onsets}) +paradigm = pd.DataFrame({'trial_type': conditions, 'onset': onsets, + 'duration': duration}) hrf_model = 'FIR' X3 = make_design_matrix(frame_times, paradigm, hrf_model='fir', drift_model='polynomial', drift_order=3, From 3fa7e0aab87fb790c75609eb92e0a3e087eee7b2 Mon Sep 17 00:00:00 2001 From: bthirion Date: Fri, 5 Oct 2018 23:16:11 +0200 Subject: [PATCH 148/210] Conflict merges with current master + a few fixes --- .../plot_localizer_surface_analysis.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/examples/02_first_level_models/plot_localizer_surface_analysis.py b/examples/02_first_level_models/plot_localizer_surface_analysis.py index 9b987492..bb327b50 100644 --- a/examples/02_first_level_models/plot_localizer_surface_analysis.py +++ b/examples/02_first_level_models/plot_localizer_surface_analysis.py @@ -14,7 +14,7 @@ then contrast estimation) The result of the analysis are statistical maps that are defined on -the brain mesh. We disply them using Nilearn capabilities. +the brain mesh. We display them using Nilearn capabilities. The projection of fMRI data onto a given brain mesh requires that both are initially defined in the same space. @@ -40,13 +40,13 @@ ######################################################################### # Prepare data -# First the fMRI data +# First the volume-based fMRI data. from nistats.datasets import fetch_localizer_first_level data = fetch_localizer_first_level() fmri_img = data.epi_img ######################################################################### -# Second the experimental paradigm +# Second the experimental paradigm. paradigm_file = data.paradigm import pandas as pd paradigm = pd.read_table(paradigm_file) @@ -56,8 +56,10 @@ # Project the fMRI image to the surface # ------------------------------------- # -# For this we need to get a mesh representing the geometry of the surface. -# we could use an individual mesh, but we first resort to a standard mesh, the so-called fsaverage5 template from the Freesurfer software. +# For this we need to get a mesh representing the geometry of the +# surface. we could use an individual mesh, but we first resort to a +# standard mesh, the so-called fsaverage5 template from the Freesurfer +# software. import nilearn fsaverage = nilearn.datasets.fetch_surf_fsaverage5() @@ -156,7 +158,8 @@ print(' Contrast % i out of %i: %s, right hemisphere' % (index + 1, len(contrasts), contrast_id)) # compute contrast-related statistics - contrast = compute_contrast(labels, estimates, contrast_val, contrast_type='t') + contrast = compute_contrast(labels, estimates, contrast_val, + contrast_type='t') # we present the Z-transform of the t map z_score = contrast.z_score() # we plot it on the surface, on the inflated fsaverage mesh, @@ -187,7 +190,8 @@ print(' Contrast % i out of %i: %s, left hemisphere' % (index + 1, len(contrasts), contrast_id)) # compute contrasts - contrast = compute_contrast(labels, estimates, contrast_val, contrast_type='t') + contrast = compute_contrast(labels, estimates, contrast_val, + contrast_type='t') z_score = contrast.z_score() # Plot the result plotting.plot_surf_stat_map( From fc9441ff0981fcc22c52c88420d6c277ef7337ae Mon Sep 17 00:00:00 2001 From: bthirion Date: Fri, 5 Oct 2018 23:50:29 +0200 Subject: [PATCH 149/210] Another fix --- .../01_tutorials/plot_first_level_model_details.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/examples/01_tutorials/plot_first_level_model_details.py b/examples/01_tutorials/plot_first_level_model_details.py index 1b22c0e3..2246671c 100644 --- a/examples/01_tutorials/plot_first_level_model_details.py +++ b/examples/01_tutorials/plot_first_level_model_details.py @@ -22,18 +22,13 @@ """ -import numpy as np -import pandas as pd -from nilearn import plotting -from nistats import datasets - ############################################################################### # Retrieving the data # ------------------- # # We use a so-called localizer dataset, which consists in a 5-minutes # acquisition of a fast event-related dataset. - +from nistats import datasets data = datasets.fetch_localizer_first_level() fmri_img = data.epi_img @@ -43,8 +38,14 @@ # We just get the provided file and make it BIDS-compliant. t_r = 2.4 paradigm_file = data.paradigm +import pandas as pd events= pd.read_table(paradigm_file) +############################################################################### +# Add a column for 'duration' (filled with ones) for BIDS compliance +import numpy as np +events['duration'] = np.ones_like(events.onset) + ############################################################################### # Running a basic model # --------------------- @@ -116,6 +117,7 @@ def make_localizer_contrasts(design_matrix): # Since this script will be repeated several times, for the sake of readability, # we encapsulate it in a function that we call when needed. # +from nilearn import plotting def plot_contrast(first_level_model): """ Given a first model, specify, enstimate and plot the main contrasts""" From 6fe9daadf593350d15a4e455f75967d8053341df Mon Sep 17 00:00:00 2001 From: bthirion Date: Sun, 7 Oct 2018 22:20:23 +0200 Subject: [PATCH 150/210] protocols bids-compliant again --- nistats/datasets.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/nistats/datasets.py b/nistats/datasets.py index 6bad47f0..69d7246a 100644 --- a/nistats/datasets.py +++ b/nistats/datasets.py @@ -303,6 +303,8 @@ def _make_bids_compliant_localizer_first_level_paradigm_file(paradigm_file): names=['session', 'trial_type', 'onset'], ) paradigm.drop(labels='session', axis=1, inplace=True) + # duration is required in BIDS specification + paradigm['duration'] = np.ones_like(paradigm.onset) paradigm.to_csv(paradigm_file, sep='\t', index=False) @@ -614,7 +616,9 @@ def _make_events_file_spm_multimodal_fmri(_subject_data, session): onsets *= tr # because onsets were reporting in 'scans' units conditions = (['faces'] * len(faces_onsets) + ['scrambled'] * len(scrambled_onsets)) - paradigm = pd.DataFrame({'trial_type': conditions, 'onset': onsets}) + duration = np.ones_like(onsets) + paradigm = pd.DataFrame({'trial_type': conditions, 'onset': onsets, + 'duration': duration}) return paradigm From 4dc07ce2fd5a29dedf91d979da693f189cc04903 Mon Sep 17 00:00:00 2001 From: bthirion Date: Sun, 7 Oct 2018 22:34:49 +0200 Subject: [PATCH 151/210] fixed failing test --- nistats/tests/test_datasets.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/nistats/tests/test_datasets.py b/nistats/tests/test_datasets.py index e17a0fc5..769b2389 100644 --- a/nistats/tests/test_datasets.py +++ b/nistats/tests/test_datasets.py @@ -162,13 +162,13 @@ def _input_data_for_test_file(): def _expected_output_data_from_test_file(): file_data = [ - ['calculvideo', 0.0], - ['calculvideo', 2.400000095], - ['damier_H', 8.699999809], - ['clicDaudio', 11.39999961], + ['calculvideo', 0.0, 1.0], + ['calculvideo', 2.400000095, 1.0], + ['damier_H', 8.699999809, 1.0], + ['clicDaudio', 11.39999961, 1.0], ] file_data = pd.DataFrame(file_data) - file_data.columns = ['trial_type', 'onset'] + file_data.columns = ['trial_type', 'onset', 'duration'] return file_data def run_test(): From 7e3a79711e7d166d390af99a8dfc49e9f9ef8085 Mon Sep 17 00:00:00 2001 From: bthirion Date: Sun, 7 Oct 2018 22:55:46 +0200 Subject: [PATCH 152/210] plot_localizer analysis removed, because it is redundant with other examples --- .../plot_localizer_analysis.py | 88 ------------------- 1 file changed, 88 deletions(-) delete mode 100644 examples/02_first_level_models/plot_localizer_analysis.py diff --git a/examples/02_first_level_models/plot_localizer_analysis.py b/examples/02_first_level_models/plot_localizer_analysis.py deleted file mode 100644 index 0ecca292..00000000 --- a/examples/02_first_level_models/plot_localizer_analysis.py +++ /dev/null @@ -1,88 +0,0 @@ -""" -First level analysis of localizer dataset -========================================= - -Full step-by-step example of fitting a GLM to experimental data and visualizing -the results. - -More specifically: - -1. A sequence of fMRI volumes are loaded -2. A design matrix describing all the effects related to the data is computed -3. a mask of the useful brain volume is computed -4. A GLM is applied to the dataset (effect/covariance, - then contrast estimation) - -""" -import numpy as np -import pandas as pd -from nilearn import plotting - -from nistats.first_level_model import FirstLevelModel -from nistats import datasets - -######################################################################### -# Prepare data and analysis parameters -# ------------------------------------- -# Prepare timing -t_r = 2.4 -slice_time_ref = 0.5 - -# Prepare data -data = datasets.fetch_localizer_first_level() -paradigm_file = data.paradigm -paradigm = pd.read_csv(paradigm_file, sep=' ', header=None, index_col=None) -paradigm.columns = ['session', 'trial_type', 'onset'] -paradigm['duration'] = np.ones_like(paradigm.onset) -fmri_img = data.epi_img - -######################################################################### -# Perform first level analysis -# ---------------------------- -# Setup and fit GLM -first_level_model = FirstLevelModel(t_r, slice_time_ref, - hrf_model='glover + derivative') -first_level_model = first_level_model.fit(fmri_img, paradigm) - -######################################################################### -# Estimate contrasts -# ------------------ -# Specify the contrasts -design_matrix = first_level_model.design_matrices_[0] -contrast_matrix = np.eye(design_matrix.shape[1]) -contrasts = dict([(column, contrast_matrix[i]) - for i, column in enumerate(design_matrix.columns)]) - -contrasts["audio"] = contrasts["clicDaudio"] + contrasts["clicGaudio"] +\ - contrasts["calculaudio"] + contrasts["phraseaudio"] -contrasts["video"] = contrasts["clicDvideo"] + contrasts["clicGvideo"] + \ - contrasts["calculvideo"] + contrasts["phrasevideo"] -contrasts["computation"] = contrasts["calculaudio"] + contrasts["calculvideo"] -contrasts["sentences"] = contrasts["phraseaudio"] + contrasts["phrasevideo"] - -######################################################################### -# Short list of more relevant contrasts -contrasts = { - "left-right": (contrasts["clicGaudio"] + contrasts["clicGvideo"] - - contrasts["clicDaudio"] - contrasts["clicDvideo"]), - "H-V": contrasts["damier_H"] - contrasts["damier_V"], - "audio-video": contrasts["audio"] - contrasts["video"], - "video-audio": -contrasts["audio"] + contrasts["video"], - "computation-sentences": (contrasts["computation"] - - contrasts["sentences"]), - "reading-visual": contrasts["phrasevideo"] - contrasts["damier_H"] - } - -######################################################################### -# contrast estimation -for index, (contrast_id, contrast_val) in enumerate(contrasts.items()): - print(' Contrast % 2i out of %i: %s' % - (index + 1, len(contrasts), contrast_id)) - z_map = first_level_model.compute_contrast(contrast_val, - output_type='z_score') - - # Create snapshots of the contrasts - display = plotting.plot_stat_map(z_map, display_mode='z', - threshold=3.0, title=contrast_id) - -plotting.show() From ef155576a124d9c9aa14b8a09492ba45fdc04940 Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Mon, 8 Oct 2018 11:57:06 +0200 Subject: [PATCH 153/210] (BIDS compliance) Replaced 'paradigm' with 'events', 'experimental paradigm' -1 - Replaced \ line continuation with parantheses. - Reformatted multiline ars and expressions. - Changed so 'events' is accessed using dict key, not obj attribute. - Removed paradigm_from_csv() reference in reference.rst (function removed previously) --- doc/modules/reference.rst | 3 +- .../plot_first_level_model_details.py | 30 +++++++---- .../plot_single_subject_single_run.py | 2 +- .../plot_localizer_surface_analysis.py | 51 +++++++++++-------- nistats/datasets.py | 38 +++++++------- nistats/tests/test_datasets.py | 6 +-- 6 files changed, 73 insertions(+), 57 deletions(-) diff --git a/doc/modules/reference.rst b/doc/modules/reference.rst index 1b3cb41a..4f5b671e 100644 --- a/doc/modules/reference.rst +++ b/doc/modules/reference.rst @@ -99,8 +99,7 @@ uses. :toctree: generated/ :template: function.rst - check_paradigm - paradigm_from_csv + check_events .. _model_ref: diff --git a/examples/01_tutorials/plot_first_level_model_details.py b/examples/01_tutorials/plot_first_level_model_details.py index 2246671c..541afd3e 100644 --- a/examples/01_tutorials/plot_first_level_model_details.py +++ b/examples/01_tutorials/plot_first_level_model_details.py @@ -33,13 +33,13 @@ fmri_img = data.epi_img ############################################################################### -# Define the paradigm that will be used +# Define the experimental events that will be used # # We just get the provided file and make it BIDS-compliant. t_r = 2.4 -paradigm_file = data.paradigm +events_file = data['events'] import pandas as pd -events= pd.read_table(paradigm_file) +events= pd.read_table(events_file) ############################################################################### # Add a column for 'duration' (filled with ones) for BIDS compliance @@ -81,21 +81,31 @@ def make_localizer_contrasts(design_matrix): for i, column in enumerate(design_matrix.columns)]) # Add more complex contrasts - contrasts['audio'] = contrasts['clicDaudio'] + contrasts['clicGaudio'] +\ - contrasts['calculaudio'] + contrasts['phraseaudio'] - contrasts['video'] = contrasts['clicDvideo'] + contrasts['clicGvideo'] + \ - contrasts['calculvideo'] + contrasts['phrasevideo'] + contrasts['audio'] = (contrasts['clicDaudio'] + + contrasts['clicGaudio'] + + contrasts['calculaudio'] + + contrasts['phraseaudio'] + ) + contrasts['video'] = (contrasts['clicDvideo'] + + contrasts['clicGvideo'] + + contrasts['calculvideo'] + + contrasts['phrasevideo'] + ) contrasts['computation'] = contrasts['calculaudio'] + contrasts['calculvideo'] contrasts['sentences'] = contrasts['phraseaudio'] + contrasts['phrasevideo'] # Short dictionary of more relevant contrasts contrasts = { - 'left-right': (contrasts['clicGaudio'] + contrasts['clicGvideo'] - - contrasts['clicDaudio'] - contrasts['clicDvideo']), + 'left-right': (contrasts['clicGaudio'] + + contrasts['clicGvideo'] + - contrasts['clicDaudio'] + - contrasts['clicDvideo'] + ), 'H-V': contrasts['damier_H'] - contrasts['damier_V'], 'audio-video': contrasts['audio'] - contrasts['video'], 'computation-sentences': (contrasts['computation'] - - contrasts['sentences']), + contrasts['sentences'] + ), } return contrasts diff --git a/examples/01_tutorials/plot_single_subject_single_run.py b/examples/01_tutorials/plot_single_subject_single_run.py index 236f0275..dafe2535 100644 --- a/examples/01_tutorials/plot_single_subject_single_run.py +++ b/examples/01_tutorials/plot_single_subject_single_run.py @@ -71,7 +71,7 @@ # provided in an events.tsv file. The path of this file is # provided in the dataset. import pandas as pd -events = pd.read_table(subject_data['paradigm']) +events = pd.read_table(subject_data['events']) print(events) ############################################################################### diff --git a/examples/02_first_level_models/plot_localizer_surface_analysis.py b/examples/02_first_level_models/plot_localizer_surface_analysis.py index bb327b50..204abe0c 100644 --- a/examples/02_first_level_models/plot_localizer_surface_analysis.py +++ b/examples/02_first_level_models/plot_localizer_surface_analysis.py @@ -47,10 +47,9 @@ ######################################################################### # Second the experimental paradigm. -paradigm_file = data.paradigm +events_file = data['events'] import pandas as pd -paradigm = pd.read_table(paradigm_file) -fmri_img = data.epi_img +events = pd.read_table(events_file) ######################################################################### # Project the fMRI image to the surface @@ -87,8 +86,10 @@ # We specify an hrf model containing Glover model and its time derivative # the drift model is implicitly a cosine basis with period cutoff 128s. from nistats.design_matrix import make_design_matrix -design_matrix = make_design_matrix( - frame_times, paradigm=paradigm, hrf_model='glover + derivative') +design_matrix = make_design_matrix(frame_times, + paradigm=events, + hrf_model='glover + derivative' + ) ######################################################################### # Setup and fit GLM. @@ -115,18 +116,22 @@ ######################################################################### # add some intermediate contrasts -basic_contrasts["audio"] = basic_contrasts["clicDaudio"] +\ - basic_contrasts["clicGaudio"] +\ - basic_contrasts["calculaudio"] +\ - basic_contrasts["phraseaudio"] -basic_contrasts["video"] = basic_contrasts["clicDvideo"] +\ - basic_contrasts["clicGvideo"] + \ - basic_contrasts["calculvideo"] +\ - basic_contrasts["phrasevideo"] -basic_contrasts["computation"] = basic_contrasts["calculaudio"] +\ - basic_contrasts["calculvideo"] -basic_contrasts["sentences"] = basic_contrasts["phraseaudio"] +\ - basic_contrasts["phrasevideo"] +basic_contrasts["audio"] = (basic_contrasts["clicDaudio"] + + basic_contrasts["clicGaudio"] + + basic_contrasts["calculaudio"] + + basic_contrasts["phraseaudio"] + ) +basic_contrasts["video"] = (basic_contrasts["clicDvideo"] + + basic_contrasts["clicGvideo"] + + basic_contrasts["calculvideo"] + + basic_contrasts["phrasevideo"] + ) +basic_contrasts["computation"] = (basic_contrasts["calculaudio"] + + basic_contrasts["calculvideo"] + ) +basic_contrasts["sentences"] = (basic_contrasts["phraseaudio"] + + basic_contrasts["phrasevideo"] + ) ######################################################################### # Finally make a dictionary of more relevant contrasts @@ -138,13 +143,15 @@ # Of course, we could define other contrasts, but we keep only 3 for simplicity. contrasts = { - "left - right button press": (basic_contrasts["clicGaudio"] + - basic_contrasts["clicGvideo"] - - basic_contrasts["clicDaudio"] - - basic_contrasts["clicDvideo"]), + "left - right button press": (basic_contrasts["clicGaudio"] + + basic_contrasts["clicGvideo"] + - basic_contrasts["clicDaudio"] + - basic_contrasts["clicDvideo"] + ), "audio - video": basic_contrasts["audio"] - basic_contrasts["video"], "computation - sentences": (basic_contrasts["computation"] - - basic_contrasts["sentences"]) + basic_contrasts["sentences"] + ) } ######################################################################### diff --git a/nistats/datasets.py b/nistats/datasets.py index 69d7246a..76c3f804 100644 --- a/nistats/datasets.py +++ b/nistats/datasets.py @@ -283,7 +283,7 @@ def fetch_openneuro_dataset( return data_dir, sorted(downloaded) -def _make_bids_compliant_localizer_first_level_paradigm_file(paradigm_file): +def _make_events_file_localizer_first_level(events_file): """ Makes the first-level localizer fMRI dataset events file BIDS compliant. Overwrites the original file. Adds headers in first row. @@ -292,20 +292,20 @@ def _make_bids_compliant_localizer_first_level_paradigm_file(paradigm_file): Parameters ---------- - paradigm_file: string + events_file: string path to the localizer_first_level dataset's events file. Returns ------- None """ - paradigm = pd.read_csv(paradigm_file, sep=' ', header=None, index_col=None, + events = pd.read_csv(events_file, sep=' ', header=None, index_col=None, names=['session', 'trial_type', 'onset'], ) - paradigm.drop(labels='session', axis=1, inplace=True) + events.drop(labels='session', axis=1, inplace=True) # duration is required in BIDS specification - paradigm['duration'] = np.ones_like(paradigm.onset) - paradigm.to_csv(paradigm_file, sep='\t', index=False) + events['duration'] = np.ones_like(events.onset) + events.to_csv(events_file, sep='\t', index=False) def fetch_localizer_first_level(data_dir=None, verbose=1): @@ -319,15 +319,15 @@ def fetch_localizer_first_level(data_dir=None, verbose=1): Returns ------- data: sklearn.datasets.base.Bunch - dictionary-like object, keys are: + dictionary-like object, with the keys: epi_img: the input 4D image - paradigm: a csv file describing the paardigm + events: a csv file describing the paardigm """ url = 'ftp://ftp.cea.fr/pub/dsv/madic/download/nipy' dataset_name = "localizer_first_level" files = dict(epi_img="s12069_swaloc1_corr.nii.gz", - paradigm="localizer_paradigm.csv") + events="localizer_paradigm.csv") # The options needed for _fetch_files options = [(filename, os.path.join(url, filename), {}) for _, filename in sorted(files.items())] @@ -339,11 +339,11 @@ def fetch_localizer_first_level(data_dir=None, verbose=1): params = dict(zip(sorted(files.keys()), sub_files)) try: - _verify_events_file_uses_tab_separators(params['paradigm']) + _verify_events_file_uses_tab_separators(params['events']) except ValueError: - _make_bids_compliant_localizer_first_level_paradigm_file(paradigm_file= - params['paradigm'] - ) + _make_events_file_localizer_first_level(events_file= + params['events'] + ) return Bunch(**params) @@ -491,13 +491,13 @@ def fetch_spm_auditory(data_dir=None, data_name='spm_auditory', _download_spm_auditory_data(data_dir, subject_dir, subject_id) spm_auditory_data = _prepare_downloaded_spm_auditory_data(subject_dir) try: - spm_auditory_data['paradigm'] + spm_auditory_data['events'] except KeyError: events_filepath = _make_path_events_file_spm_auditory_data( spm_auditory_data) if not os.path.isfile(events_filepath): _make_events_file_spm_auditory_data(events_filepath) - spm_auditory_data['paradigm'] = events_filepath + spm_auditory_data['events'] = events_filepath return spm_auditory_data @@ -555,12 +555,12 @@ def _glob_spm_multimodal_fmri_data(subject_dir): if not _subject_data: return None try: - paradigm = _make_events_file_spm_multimodal_fmri(_subject_data, session) + events = _make_events_file_spm_multimodal_fmri(_subject_data, session) except MatReadError as mat_err: warnings.warn('{}. An events.tsv file cannot be generated'.format(str(mat_err))) else: events_filepath = _make_events_filepath_spm_multimodal_fmri(_subject_data, session) - paradigm.to_csv(events_filepath, sep='\t', index=False) + events.to_csv(events_filepath, sep='\t', index=False) _subject_data['events{}'.format(session)] = events_filepath @@ -617,9 +617,9 @@ def _make_events_file_spm_multimodal_fmri(_subject_data, session): conditions = (['faces'] * len(faces_onsets) + ['scrambled'] * len(scrambled_onsets)) duration = np.ones_like(onsets) - paradigm = pd.DataFrame({'trial_type': conditions, 'onset': onsets, + events = pd.DataFrame({'trial_type': conditions, 'onset': onsets, 'duration': duration}) - return paradigm + return events def fetch_spm_multimodal_fmri(data_dir=None, data_name="spm_multimodal_fmri", diff --git a/nistats/tests/test_datasets.py b/nistats/tests/test_datasets.py index 769b2389..842cb712 100644 --- a/nistats/tests/test_datasets.py +++ b/nistats/tests/test_datasets.py @@ -150,7 +150,7 @@ def test_fetch_openneuro_dataset(): assert_true(len(dl_files) == 9) -def test_make_bids_compliant_localizer_first_level_paradigm_file(): +def test_make_events_file_localizer_first_level(): def _input_data_for_test_file(): file_data = [ [0, 'calculvideo', 0.0], @@ -182,7 +182,7 @@ def run_test(): header=False, sep=' ', ) - datasets._make_bids_compliant_localizer_first_level_paradigm_file( + datasets._make_events_file_localizer_first_level( temp_csv_obj.name ) data_from_test_file_post_mod = pd.read_csv(temp_csv_obj.name, @@ -198,7 +198,7 @@ def run_test(): @with_setup(setup_mock, teardown_mock) def test_fetch_localizer(): dataset = datasets.fetch_localizer_first_level() - assert_true(isinstance(dataset.paradigm, _basestring)) + assert_true(isinstance(dataset['events'], _basestring)) assert_true(isinstance(dataset.epi_img, _basestring)) From 676c6c7833bc34348528f9c4a5f10f97f2e20bb4 Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Mon, 8 Oct 2018 14:08:11 +0200 Subject: [PATCH 154/210] Examples write events.tsv files, paradigm replaced by events (BIDS compliance) - Replaced .csv file extension for events file with .tsv . - In examples, generally replaced paradigm.to_csv('paradigm.csv') with events.to_csv('events.tsv', sep='\t', index=False). - Tweaked documentation language. - Typo correction. --- .../plot_localizer_surface_analysis.py | 2 +- .../plot_spm_multimodal_faces.py | 2 +- .../plot_design_matrix.py | 18 +++++----- ..._paradigm_file.py => write_events_file.py} | 10 +++--- nistats/datasets.py | 2 +- nistats/design_matrix.py | 36 +++++++++---------- nistats/experimental_paradigm.py | 36 +++++++++---------- 7 files changed, 53 insertions(+), 53 deletions(-) rename examples/04_low_level_functions/{write_paradigm_file.py => write_events_file.py} (92%) diff --git a/examples/02_first_level_models/plot_localizer_surface_analysis.py b/examples/02_first_level_models/plot_localizer_surface_analysis.py index 204abe0c..8e0b8642 100644 --- a/examples/02_first_level_models/plot_localizer_surface_analysis.py +++ b/examples/02_first_level_models/plot_localizer_surface_analysis.py @@ -87,7 +87,7 @@ # the drift model is implicitly a cosine basis with period cutoff 128s. from nistats.design_matrix import make_design_matrix design_matrix = make_design_matrix(frame_times, - paradigm=events, + events=events, hrf_model='glover + derivative' ) diff --git a/examples/02_first_level_models/plot_spm_multimodal_faces.py b/examples/02_first_level_models/plot_spm_multimodal_faces.py index 430032e8..354b820d 100644 --- a/examples/02_first_level_models/plot_spm_multimodal_faces.py +++ b/examples/02_first_level_models/plot_spm_multimodal_faces.py @@ -61,7 +61,7 @@ ######################################################################### # loop over the two sessions for idx, img in enumerate(fmri_img, start=1): - # Build paradigm + # Build experimental paradigm n_scans = img.shape[-1] events = pd.read_table(subject_data['events{}'.format(idx)]) # Define the sampling times for the design matrix diff --git a/examples/04_low_level_functions/plot_design_matrix.py b/examples/04_low_level_functions/plot_design_matrix.py index 4f87a22e..49448561 100644 --- a/examples/04_low_level_functions/plot_design_matrix.py +++ b/examples/04_low_level_functions/plot_design_matrix.py @@ -22,7 +22,7 @@ import numpy as np tr = 1.0 # repetition time is 1 second n_scans = 128 # the acquisition comprises 128 scans -frame_times = np.arange(n_scans) * tr # here are the corespoding frame times +frame_times = np.arange(n_scans) * tr # here are the correspoding frame times ######################################################################### # then we define parameters related to the experimental design @@ -42,37 +42,37 @@ # Create design matrices # ------------------------------------- # The same parameters allow us to obtain a variety of design matrices -# We first create an event object +# We first create an events object import pandas as pd -paradigm = pd.DataFrame({'trial_type': conditions, 'onset': onsets, +events = pd.DataFrame({'trial_type': conditions, 'onset': onsets, 'duration': duration}) ######################################################################### -# We sample the paradigm into a design matrix, also including additional regressors +# We sample the events into a design matrix, also including additional regressors hrf_model = 'glover' from nistats.design_matrix import make_design_matrix X1 = make_design_matrix( - frame_times, paradigm, drift_model='polynomial', drift_order=3, + frame_times, events, drift_model='polynomial', drift_order=3, add_regs=motion, add_reg_names=add_reg_names, hrf_model=hrf_model) ######################################################################### # Now we compute a block design matrix. We add duration to create the blocks. # For this we first define an event structure that includes the duration parameter duration = 7. * np.ones(len(conditions)) -paradigm = pd.DataFrame({'trial_type': conditions, 'onset': onsets, +events = pd.DataFrame({'trial_type': conditions, 'onset': onsets, 'duration': duration}) ######################################################################### # Then we sample the design matrix -X2 = make_design_matrix(frame_times, paradigm, drift_model='polynomial', +X2 = make_design_matrix(frame_times, events, drift_model='polynomial', drift_order=3, hrf_model=hrf_model) ######################################################################### # Finally we compute a FIR model -paradigm = pd.DataFrame({'trial_type': conditions, 'onset': onsets, +events = pd.DataFrame({'trial_type': conditions, 'onset': onsets, 'duration': duration}) hrf_model = 'FIR' -X3 = make_design_matrix(frame_times, paradigm, hrf_model='fir', +X3 = make_design_matrix(frame_times, events, hrf_model='fir', drift_model='polynomial', drift_order=3, fir_delays=np.arange(1, 6)) diff --git a/examples/04_low_level_functions/write_paradigm_file.py b/examples/04_low_level_functions/write_events_file.py similarity index 92% rename from examples/04_low_level_functions/write_paradigm_file.py rename to examples/04_low_level_functions/write_events_file.py index 60a334e9..65ba3248 100644 --- a/examples/04_low_level_functions/write_paradigm_file.py +++ b/examples/04_low_level_functions/write_events_file.py @@ -1,4 +1,4 @@ -"""Example of a paradigm .csv file generation: the neurospin/localizer paradigm. +"""Example of a events.tsv file generation: the neurospin/localizer events. ============================================================================= The protocol described is the so-called "archi standard" localizer @@ -59,7 +59,7 @@ 'duration': duration}) ######################################################################### -# Export them to a csv file -csvfile = 'localizer_paradigm.csv' -events.to_csv(csvfile) -print("Created the paradigm file in %s " % csvfile) +# Export them to a tsv file +tsvfile = 'localizer_events.tsv' +events.to_csv(tsvfile, sep='\t', index=False) +print("Created the events file in %s " % tsvfile) diff --git a/nistats/datasets.py b/nistats/datasets.py index 76c3f804..a6f4c3aa 100644 --- a/nistats/datasets.py +++ b/nistats/datasets.py @@ -327,7 +327,7 @@ def fetch_localizer_first_level(data_dir=None, verbose=1): dataset_name = "localizer_first_level" files = dict(epi_img="s12069_swaloc1_corr.nii.gz", - events="localizer_paradigm.csv") + events="localizer_events.tsv") # The options needed for _fetch_files options = [(filename, os.path.join(url, filename), {}) for _, filename in sorted(files.items())] diff --git a/nistats/design_matrix.py b/nistats/design_matrix.py index 4de3dea2..94364642 100644 --- a/nistats/design_matrix.py +++ b/nistats/design_matrix.py @@ -41,7 +41,7 @@ import pandas as pd from .hemodynamic_models import compute_regressor, _orthogonalize -from .experimental_paradigm import check_paradigm +from .experimental_paradigm import check_events from .utils import full_rank, _basestring ###################################################################### @@ -163,15 +163,15 @@ def _make_drift(drift_model, frame_times, order=1, period_cut=128.): return drift, names -def _convolve_regressors(paradigm, hrf_model, frame_times, fir_delays=[0], +def _convolve_regressors(events, hrf_model, frame_times, fir_delays=[0], min_onset=-24, oversampling=50): """ Creation of a matrix that comprises the convolution of the conditions onset with a certain hrf model Parameters ---------- - paradigm : DataFrame instance, - Descriptor of an experimental paradigm + events : DataFrame instance, + Events data describing the experimental paradigm see nistats.experimental_paradigm to check the specification for these to be valid paradigm descriptors @@ -221,7 +221,7 @@ def _convolve_regressors(paradigm, hrf_model, frame_times, fir_delays=[0], elif oversampling is None: oversampling = 50 - trial_type, onset, duration, modulation = check_paradigm(paradigm) + trial_type, onset, duration, modulation = check_events(events) for condition in np.unique(trial_type): condition_mask = (trial_type == condition) exp_condition = (onset[condition_mask], @@ -279,7 +279,7 @@ def _full_rank(X, cmax=1e15): def make_design_matrix( - frame_times, paradigm=None, hrf_model='glover', + frame_times, events=None, hrf_model='glover', drift_model='cosine', period_cut=128, drift_order=1, fir_delays=[0], add_regs=None, add_reg_names=None, min_onset=-24, oversampling=50): """Generate a design matrix from the input parameters @@ -289,11 +289,11 @@ def make_design_matrix( frame_times : array of shape (n_frames,) The timing of acquisition of the scans in seconds. - paradigm : DataFrame instance, optional - Description of the experimental paradigm. The DataFrame instance might - have those keys: + events : DataFrame instance, optional + Events data that describes the experimental paradigm. + The DataFrame instance might have these keys: 'onset': column to specify the start time of each events in - seconds. An exception is raised if this key is missing. + seconds. An error is raised if this key is missing. 'trial_type': column to specify per-event experimental conditions identifier. If missing each event are labelled 'dummy' and considered to form a unique condition. @@ -303,11 +303,11 @@ def make_design_matrix( 'modulation': column to specify the amplitude of each events. If missing the default is set to ones(n_events). - A paradigm is considered as valid whenever it has an 'onset' key, if - this key is missing an exception will be thrown. For the others keys - only a simple warning will be displayed. A particular attention should - be given to the 'trial_type' key which defines the different conditions - in the paradigm. + An experimental paradigm is valid if it has an 'onset' key. + If this key is missing an error will be raised. + For the others keys a warning will be displayed. + Particular attention should be given to the 'trial_type' key + which defines the different conditions in the experimental paradigm. hrf_model : {'spm', 'spm + derivative', 'spm + derivative + dispersion', 'glover', 'glover + derivative', 'glover + derivative + dispersion', @@ -374,13 +374,13 @@ def make_design_matrix( names = [] matrix = None - # step 1: paradigm-related regressors - if paradigm is not None: + # step 1: events-related regressors + if events is not None: # create the condition-related regressors if isinstance(hrf_model, _basestring): hrf_model = hrf_model.lower() matrix, names = _convolve_regressors( - paradigm, hrf_model, frame_times, fir_delays, min_onset, + events, hrf_model, frame_times, fir_delays, min_onset, oversampling) # step 2: additional regressors diff --git a/nistats/experimental_paradigm.py b/nistats/experimental_paradigm.py index fac895cb..34ec3805 100644 --- a/nistats/experimental_paradigm.py +++ b/nistats/experimental_paradigm.py @@ -4,7 +4,7 @@ An experimental protocol is handled as a pandas DataFrame that includes an 'onset' field. -This yields the onset time of the events in the paradigm. +This yields the onset time of the events in the experimental paradigm. It can also contain: * a 'trial_type' field that yields the condition identifier. * a 'duration' field that yields event duration (for so-called block @@ -19,15 +19,15 @@ import warnings -def check_paradigm(paradigm): - """Test that the DataFrame is describes a valid experimental paradigm +def check_events(events): + """Test that the events data describes a valid experimental paradigm - A DataFrame is considered as valid whenever it has an 'onset' key. + It is valid if the events data has an 'onset' key. Parameters ---------- - paradigm : pandas DataFrame - Describes a functional paradigm. + events : pandas DataFrame + Events data that describes a functional experimental paradigm. Returns ------- @@ -46,20 +46,20 @@ def check_paradigm(paradigm): Per-event modulation, (in seconds) defaults to ones(n_events) when no duration is provided """ - if 'onset' not in paradigm.keys(): - raise ValueError('The provided paradigm has no onset key') - if 'duration' not in paradigm.keys(): - raise ValueError('The provided paradigm has no duration key') + if 'onset' not in events.keys(): + raise ValueError('The provided events data has no onset column.') + if 'duration' not in events.keys(): + raise ValueError('The provided events data has no duration column.') - onset = np.array(paradigm['onset']) - duration = np.array(paradigm['duration']).astype(np.float) + onset = np.array(events['onset']) + duration = np.array(events['duration']).astype(np.float) n_events = len(onset) - trial_type = np.array(paradigm['trial_type']) + trial_type = np.array(events['trial_type']) modulation = np.ones(n_events) - if 'trial_type' not in paradigm.keys(): - warnings.warn("'trial_type' key not found in the given paradigm.") + if 'trial_type' not in events.keys(): + warnings.warn("'trial_type' column not found in the given events data.") trial_type = np.repeat('dummy', n_events) - if 'modulation' in paradigm.keys(): - warnings.warn("'modulation' key found in the given paradigm.") - modulation = np.array(paradigm['modulation']).astype(np.float) + if 'modulation' in events.keys(): + warnings.warn("'modulation' column found in the given events data.") + modulation = np.array(events['modulation']).astype(np.float) return trial_type, onset, duration, modulation From 2bddf37586566a67adf024aae82343a096392005 Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Mon, 8 Oct 2018 14:25:06 +0200 Subject: [PATCH 155/210] Replaced paradigm with events, experimental paradigm in variable names, docs --- nistats/tests/test_dmtx.py | 156 ++++++++++++------------ nistats/tests/test_first_level_model.py | 18 +-- nistats/tests/test_paradigm.py | 38 +++--- 3 files changed, 106 insertions(+), 106 deletions(-) diff --git a/nistats/tests/test_dmtx.py b/nistats/tests/test_dmtx.py index 8d775f03..42581339 100644 --- a/nistats/tests/test_dmtx.py +++ b/nistats/tests/test_dmtx.py @@ -30,12 +30,12 @@ def design_matrix_light( - frame_times, paradigm=None, hrf_model='glover', + frame_times, events=None, hrf_model='glover', drift_model='cosine', period_cut=128, drift_order=1, fir_delays=[0], add_regs=None, add_reg_names=None, min_onset=-24, path=None): """ Idem make_design_matrix, but only returns the computed matrix and associated names """ - dmtx = make_design_matrix(frame_times, paradigm, hrf_model, + dmtx = make_design_matrix(frame_times, events, hrf_model, drift_model, period_cut, drift_order, fir_delays, add_regs, add_reg_names, min_onset) _, matrix, names = check_design_matrix(dmtx) @@ -46,10 +46,10 @@ def basic_paradigm(): conditions = ['c0', 'c0', 'c0', 'c1', 'c1', 'c1', 'c2', 'c2', 'c2'] onsets = [30, 70, 100, 10, 30, 90, 30, 40, 60] durations = 1 * np.ones(9) - paradigm = pd.DataFrame({'trial_type': conditions, + events = pd.DataFrame({'trial_type': conditions, 'onset': onsets, 'duration': durations}) - return paradigm + return events def modulated_block_paradigm(): @@ -57,11 +57,11 @@ def modulated_block_paradigm(): onsets = [30, 70, 100, 10, 30, 90, 30, 40, 60] durations = 5 + 5 * np.random.rand(len(onsets)) values = 1 + np.random.rand(len(onsets)) - paradigm = pd.DataFrame({'trial_type': conditions, + events = pd.DataFrame({'trial_type': conditions, 'onset': onsets, 'duration': durations, 'modulation': values}) - return paradigm + return events def modulated_event_paradigm(): @@ -69,21 +69,21 @@ def modulated_event_paradigm(): onsets = [30, 70, 100, 10, 30, 90, 30, 40, 60] durations = 1 * np.ones(9) values = 1 + np.random.rand(len(onsets)) - paradigm = pd.DataFrame({'trial_type': conditions, + events = pd.DataFrame({'trial_type': conditions, 'onset': onsets, 'duration': durations, 'modulation': values}) - return paradigm + return events def block_paradigm(): conditions = ['c0', 'c0', 'c0', 'c1', 'c1', 'c1', 'c2', 'c2', 'c2'] onsets = [30, 70, 100, 10, 30, 90, 30, 40, 60] durations = 5 * np.ones(9) - paradigm = pd.DataFrame({'trial_type': conditions, + events = pd.DataFrame({'trial_type': conditions, 'onset': onsets, 'duration': durations}) - return paradigm + return events def test_cosine_drift(): @@ -98,7 +98,7 @@ def test_cosine_drift(): def test_design_matrix0(): - # Test design matrix creation when no paradigm is provided + # Test design matrix creation when no experimental paradigm is provided tr = 1.0 frame_times = np.linspace(0, 127 * tr, 128) _, X, names = check_design_matrix(make_design_matrix( @@ -145,11 +145,11 @@ def test_convolve_regressors(): conditions = ['c0', 'c1'] onsets = [20, 40] duration = [1, 1] - paradigm = pd.DataFrame( + events = pd.DataFrame( {'trial_type': conditions, 'onset': onsets, 'duration': duration}) # names not passed -> default names frame_times = np.arange(100) - f, names = _convolve_regressors(paradigm, 'glover', frame_times) + f, names = _convolve_regressors(events, 'glover', frame_times) assert_equal(names, ['c0', 'c1']) @@ -157,9 +157,9 @@ def test_design_matrix1(): # basic test based on basic_paradigm and glover hrf tr = 1.0 frame_times = np.linspace(0, 127 * tr, 128) - paradigm = basic_paradigm() + events = basic_paradigm() hrf_model = 'glover' - X, names = design_matrix_light(frame_times, paradigm, hrf_model=hrf_model, + X, names = design_matrix_light(frame_times, events, hrf_model=hrf_model, drift_model='polynomial', drift_order=3) assert_equal(len(names), 7) assert_equal(X.shape, (128, 7)) @@ -171,9 +171,9 @@ def test_design_matrix2(): # idem test_design_matrix1 with a different drift term tr = 1.0 frame_times = np.linspace(0, 127 * tr, 128) - paradigm = basic_paradigm() + events = basic_paradigm() hrf_model = 'glover' - X, names = design_matrix_light(frame_times, paradigm, hrf_model=hrf_model, + X, names = design_matrix_light(frame_times, events, hrf_model=hrf_model, drift_model='cosine', period_cut=63) assert_equal(len(names), 7) # was 8 with old cosine @@ -182,9 +182,9 @@ def test_design_matrix3(): # idem test_design_matrix1 with a different drift term tr = 1.0 frame_times = np.linspace(0, 127 * tr, 128) - paradigm = basic_paradigm() + events = basic_paradigm() hrf_model = 'glover' - X, names = design_matrix_light(frame_times, paradigm, hrf_model=hrf_model, + X, names = design_matrix_light(frame_times, events, hrf_model=hrf_model, drift_model=None) assert_equal(len(names), 4) @@ -193,48 +193,48 @@ def test_design_matrix4(): # idem test_design_matrix1 with a different hrf model tr = 1.0 frame_times = np.linspace(0, 127 * tr, 128) - paradigm = basic_paradigm() + events = basic_paradigm() hrf_model = 'glover + derivative' - X, names = design_matrix_light(frame_times, paradigm, hrf_model=hrf_model, + X, names = design_matrix_light(frame_times, events, hrf_model=hrf_model, drift_model='polynomial', drift_order=3) assert_equal(len(names), 10) def test_design_matrix5(): - # idem test_design_matrix1 with a block paradigm + # idem test_design_matrix1 with a block experimental paradigm tr = 1.0 frame_times = np.linspace(0, 127 * tr, 128) - paradigm = block_paradigm() + events = block_paradigm() hrf_model = 'glover' - X, names = design_matrix_light(frame_times, paradigm, hrf_model=hrf_model, + X, names = design_matrix_light(frame_times, events, hrf_model=hrf_model, drift_model='polynomial', drift_order=3) assert_equal(len(names), 7) def test_design_matrix6(): - # idem test_design_matrix1 with a block paradigm and the hrf derivative + # idem test_design_matrix1 with a block experimental paradigm and the hrf derivative tr = 1.0 frame_times = np.linspace(0, 127 * tr, 128) - paradigm = block_paradigm() + events = block_paradigm() hrf_model = 'glover + derivative' - X, names = design_matrix_light(frame_times, paradigm, hrf_model=hrf_model, + X, names = design_matrix_light(frame_times, events, hrf_model=hrf_model, drift_model='polynomial', drift_order=3) assert_equal(len(names), 10) def test_design_matrix7(): - # idem test_design_matrix1, but odd paradigm + # idem test_design_matrix1, but odd experimental paradigm tr = 1.0 frame_times = np.linspace(0, 127 * tr, 128) conditions = [0, 0, 0, 1, 1, 1, 3, 3, 3] durations = 1 * np.ones(9) # no condition 'c2' onsets = [30, 70, 100, 10, 30, 90, 30, 40, 60] - paradigm = pd.DataFrame({'trial_type': conditions, + events = pd.DataFrame({'trial_type': conditions, 'onset': onsets, 'duration':durations}) hrf_model = 'glover' - X, names = design_matrix_light(frame_times, paradigm, hrf_model=hrf_model, + X, names = design_matrix_light(frame_times, events, hrf_model=hrf_model, drift_model='polynomial', drift_order=3) assert_equal(len(names), 7) @@ -243,9 +243,9 @@ def test_design_matrix8(): # basic test based on basic_paradigm and FIR tr = 1.0 frame_times = np.linspace(0, 127 * tr, 128) - paradigm = basic_paradigm() + events = basic_paradigm() hrf_model = 'FIR' - X, names = design_matrix_light(frame_times, paradigm, hrf_model=hrf_model, + X, names = design_matrix_light(frame_times, events, hrf_model=hrf_model, drift_model='polynomial', drift_order=3) assert_equal(len(names), 7) @@ -254,9 +254,9 @@ def test_design_matrix9(): # basic test based on basic_paradigm and FIR tr = 1.0 frame_times = np.linspace(0, 127 * tr, 128) - paradigm = basic_paradigm() + events = basic_paradigm() hrf_model = 'FIR' - X, names = design_matrix_light(frame_times, paradigm, hrf_model=hrf_model, + X, names = design_matrix_light(frame_times, events, hrf_model=hrf_model, drift_model='polynomial', drift_order=3, fir_delays=range(1, 5)) assert_equal(len(names), 16) @@ -266,12 +266,12 @@ def test_design_matrix10(): # Check that the first column o FIR design matrix is OK tr = 1.0 frame_times = np.linspace(0, 127 * tr, 128) - paradigm = basic_paradigm() + events = basic_paradigm() hrf_model = 'FIR' - X, names = design_matrix_light(frame_times, paradigm, hrf_model=hrf_model, + X, names = design_matrix_light(frame_times, events, hrf_model=hrf_model, drift_model='polynomial', drift_order=3, fir_delays=range(1, 5)) - onset = paradigm.onset[paradigm.trial_type == 'c0'].astype(np.int) + onset = events.onset[events.trial_type == 'c0'].astype(np.int) assert_true(np.all((X[onset + 1, 0] == 1))) @@ -279,12 +279,12 @@ def test_design_matrix11(): # check that the second column of the FIR design matrix is OK indeed tr = 1.0 frame_times = np.linspace(0, 127 * tr, 128) - paradigm = basic_paradigm() + events = basic_paradigm() hrf_model = 'FIR' - X, names = design_matrix_light(frame_times, paradigm, hrf_model=hrf_model, + X, names = design_matrix_light(frame_times, events, hrf_model=hrf_model, drift_model='polynomial', drift_order=3, fir_delays=range(1, 5)) - onset = paradigm.onset[paradigm.trial_type == 'c0'].astype(np.int) + onset = events.onset[events.trial_type == 'c0'].astype(np.int) assert_true(np.all(X[onset + 3, 2] == 1)) @@ -292,12 +292,12 @@ def test_design_matrix12(): # check that the 11th column of a FIR design matrix is indeed OK tr = 1.0 frame_times = np.linspace(0, 127 * tr, 128) - paradigm = basic_paradigm() + events = basic_paradigm() hrf_model = 'FIR' - X, names = design_matrix_light(frame_times, paradigm, hrf_model=hrf_model, + X, names = design_matrix_light(frame_times, events, hrf_model=hrf_model, drift_model='polynomial', drift_order=3, fir_delays=range(1, 5)) - onset = paradigm.onset[paradigm.trial_type == 'c2'].astype(np.int) + onset = events.onset[events.trial_type == 'c2'].astype(np.int) assert_true(np.all(X[onset + 4, 11] == 1)) @@ -305,12 +305,12 @@ def test_design_matrix13(): # Check that the fir_duration is well taken into account tr = 1.0 frame_times = np.linspace(0, 127 * tr, 128) - paradigm = basic_paradigm() + events = basic_paradigm() hrf_model = 'FIR' - X, names = design_matrix_light(frame_times, paradigm, hrf_model=hrf_model, + X, names = design_matrix_light(frame_times, events, hrf_model=hrf_model, drift_model='polynomial', drift_order=3, fir_delays=range(1, 5)) - onset = paradigm.onset[paradigm.trial_type == 'c0'].astype(np.int) + onset = events.onset[events.trial_type == 'c0'].astype(np.int) assert_true(np.all(X[onset + 1, 0] == 1)) @@ -319,12 +319,12 @@ def test_design_matrix14(): # time shift tr = 1.0 frame_times = np.linspace(0, 127 * tr, 128) + tr / 2 - paradigm = basic_paradigm() + events = basic_paradigm() hrf_model = 'FIR' - X, names = design_matrix_light(frame_times, paradigm, hrf_model=hrf_model, + X, names = design_matrix_light(frame_times, events, hrf_model=hrf_model, drift_model='polynomial', drift_order=3, fir_delays=range(1, 5)) - onset = paradigm.onset[paradigm.trial_type == 'c0'].astype(np.int) + onset = events.onset[events.trial_type == 'c0'].astype(np.int) assert_true(np.all(X[onset + 1, 0] > .9)) @@ -332,10 +332,10 @@ def test_design_matrix15(): # basic test based on basic_paradigm, plus user supplied regressors tr = 1.0 frame_times = np.linspace(0, 127 * tr, 128) - paradigm = basic_paradigm() + events = basic_paradigm() hrf_model = 'glover' ax = np.random.randn(128, 4) - X, names = design_matrix_light(frame_times, paradigm, hrf_model=hrf_model, + X, names = design_matrix_light(frame_times, events, hrf_model=hrf_model, drift_model='polynomial', drift_order=3, add_regs=ax) assert_equal(len(names), 11) assert_equal(X.shape[1], 11) @@ -345,10 +345,10 @@ def test_design_matrix16(): # Check that additional regressors are put at the right place tr = 1.0 frame_times = np.linspace(0, 127 * tr, 128) - paradigm = basic_paradigm() + events = basic_paradigm() hrf_model = 'glover' ax = np.random.randn(128, 4) - X, names = design_matrix_light(frame_times, paradigm, hrf_model=hrf_model, + X, names = design_matrix_light(frame_times, events, hrf_model=hrf_model, drift_model='polynomial', drift_order=3, add_regs=ax) assert_almost_equal(X[:, 3: 7], ax) @@ -357,11 +357,11 @@ def test_design_matrix17(): # Test the effect of scaling on the events tr = 1.0 frame_times = np.linspace(0, 127 * tr, 128) - paradigm = modulated_event_paradigm() + events = modulated_event_paradigm() hrf_model = 'glover' - X, names = design_matrix_light(frame_times, paradigm, hrf_model=hrf_model, + X, names = design_matrix_light(frame_times, events, hrf_model=hrf_model, drift_model='polynomial', drift_order=3) - ct = paradigm.onset[paradigm.trial_type == 'c0'].astype(np.int) + 1 + ct = events.onset[events.trial_type == 'c0'].astype(np.int) + 1 assert_true((X[ct, 0] > 0).all()) @@ -369,11 +369,11 @@ def test_design_matrix18(): # Test the effect of scaling on the blocks tr = 1.0 frame_times = np.linspace(0, 127 * tr, 128) - paradigm = modulated_block_paradigm() + events = modulated_block_paradigm() hrf_model = 'glover' - X, names = design_matrix_light(frame_times, paradigm, hrf_model=hrf_model, + X, names = design_matrix_light(frame_times, events, hrf_model=hrf_model, drift_model='polynomial', drift_order=3) - ct = paradigm.onset[paradigm.trial_type == 'c0'].astype(np.int) + 3 + ct = events.onset[events.trial_type == 'c0'].astype(np.int) + 3 assert_true((X[ct, 0] > 0).all()) @@ -381,21 +381,21 @@ def test_design_matrix19(): # Test the effect of scaling on a FIR model tr = 1.0 frame_times = np.linspace(0, 127 * tr, 128) - paradigm = modulated_event_paradigm() + events = modulated_event_paradigm() hrf_model = 'FIR' - X, names = design_matrix_light(frame_times, paradigm, hrf_model=hrf_model, + X, names = design_matrix_light(frame_times, events, hrf_model=hrf_model, drift_model='polynomial', drift_order=3, fir_delays=range(1, 5)) - idx = paradigm.onset[paradigm.trial_type == 0].astype(np.int) + idx = events.onset[events.trial_type == 0].astype(np.int) assert_array_equal(X[idx + 1, 0], X[idx + 2, 1]) def test_design_matrix20(): # Test for commit 10662f7 frame_times = np.arange(0, 128) # was 127 in old version of _cosine_drift - paradigm = modulated_event_paradigm() + events = modulated_event_paradigm() X, names = design_matrix_light( - frame_times, paradigm, hrf_model='glover', drift_model='cosine') + frame_times, events, hrf_model='glover', drift_model='cosine') # check that the drifts are not constant assert_true(np.all(np.diff(X[:, -2]) != 0)) @@ -405,10 +405,10 @@ def test_design_matrix21(): # basic test on repeated names of user supplied regressors tr = 1.0 frame_times = np.linspace(0, 127 * tr, 128) - paradigm = basic_paradigm() + events = basic_paradigm() hrf_model = 'glover' ax = np.random.randn(128, 4) - assert_raises(ValueError, design_matrix_light, frame_times, paradigm, + assert_raises(ValueError, design_matrix_light, frame_times, events, hrf_model=hrf_model, drift_model='polynomial', drift_order=3, add_regs=ax, add_reg_names=['aha'] * ax.shape[1]) @@ -429,14 +429,14 @@ def test_fir_block(): assert_true((X[idx + 3, 7] == 1).all()) def test_oversampling(): - paradigm = basic_paradigm() + events = basic_paradigm() frame_times = np.linspace(0, 127, 128) X1 = make_design_matrix( - frame_times, paradigm, drift_model=None) + frame_times, events, drift_model=None) X2 = make_design_matrix( - frame_times, paradigm, drift_model=None, oversampling=50) + frame_times, events, drift_model=None, oversampling=50) X3 = make_design_matrix( - frame_times, paradigm, drift_model=None, oversampling=10) + frame_times, events, drift_model=None, oversampling=10) # oversampling = 16 is the default so X2 = X1, X3 \neq X1, X3 close to X2 assert_almost_equal(X1.values, X2.values) @@ -445,10 +445,10 @@ def test_oversampling(): # fir model, oversampling is forced to 1 X4 = make_design_matrix( - frame_times, paradigm, hrf_model='fir', drift_model=None, + frame_times, events, hrf_model='fir', drift_model=None, fir_delays=range(0, 4), oversampling=1) X5 = make_design_matrix( - frame_times, paradigm, hrf_model='fir', drift_model=None, + frame_times, events, hrf_model='fir', drift_model=None, fir_delays=range(0, 4), oversampling=3) assert_almost_equal(X4.values, X5.values) @@ -456,8 +456,8 @@ def test_csv_io(): # test the csv io on design matrices tr = 1.0 frame_times = np.linspace(0, 127 * tr, 128) - paradigm = modulated_event_paradigm() - DM = make_design_matrix(frame_times, paradigm, hrf_model='glover', + events = modulated_event_paradigm() + DM = make_design_matrix(frame_times, events, hrf_model='glover', drift_model='polynomial', drift_order=3) path = 'design_matrix.csv' with InTemporaryDirectory(): @@ -477,10 +477,10 @@ def test_spm_1(): conditions = ['c0', 'c0', 'c0', 'c1', 'c1', 'c1', 'c2', 'c2', 'c2'] onsets = [30, 50, 70, 10, 30, 80, 30, 40, 60] durations = 1 * np.ones(9) - paradigm = pd.DataFrame({'trial_type': conditions, + events = pd.DataFrame({'trial_type': conditions, 'onset': onsets, 'duration': durations}) - X1 = make_design_matrix(frame_times, paradigm, drift_model=None) + X1 = make_design_matrix(frame_times, events, drift_model=None) _, matrix, _ = check_design_matrix(X1) spm_design_matrix = DESIGN_MATRIX['arr_0'] assert_true(((spm_design_matrix - matrix) ** 2).sum() / @@ -494,10 +494,10 @@ def test_spm_2(): conditions = ['c0', 'c0', 'c0', 'c1', 'c1', 'c1', 'c2', 'c2', 'c2'] onsets = [30, 50, 70, 10, 30, 80, 30, 40, 60] durations = 10 * np.ones(9) - paradigm = pd.DataFrame({'trial_type': conditions, + events = pd.DataFrame({'trial_type': conditions, 'onset': onsets, 'duration': durations}) - X1 = make_design_matrix(frame_times, paradigm, drift_model=None) + X1 = make_design_matrix(frame_times, events, drift_model=None) spm_design_matrix = DESIGN_MATRIX['arr_1'] _, matrix, _ = check_design_matrix(X1) assert_true(((spm_design_matrix - matrix) ** 2).sum() / diff --git a/nistats/tests/test_first_level_model.py b/nistats/tests/test_first_level_model.py index 9c7996a0..6fd42cd5 100644 --- a/nistats/tests/test_first_level_model.py +++ b/nistats/tests/test_first_level_model.py @@ -218,10 +218,10 @@ def basic_paradigm(): conditions = ['c0', 'c0', 'c0', 'c1', 'c1', 'c1', 'c2', 'c2', 'c2'] onsets = [30, 70, 100, 10, 30, 90, 30, 40, 60] durations = 1 * np.ones(9) - paradigm = pd.DataFrame({'trial_type': conditions, + events = pd.DataFrame({'trial_type': conditions, 'onset': onsets, 'duration': durations}) - return paradigm + return events def test_first_level_model_design_creation(): @@ -234,17 +234,17 @@ def test_first_level_model_design_creation(): # basic test based on basic_paradigm and glover hrf t_r = 1.0 slice_time_ref = 0. - paradigm = basic_paradigm() + events = basic_paradigm() model = FirstLevelModel(t_r, slice_time_ref, mask=mask, drift_model='polynomial', drift_order=3) - model = model.fit(func_img, paradigm) + model = model.fit(func_img, events) frame1, X1, names1 = check_design_matrix(model.design_matrices_[0]) # check design computation is identical n_scans = func_img.get_data().shape[3] start_time = slice_time_ref * t_r end_time = (n_scans - 1 + slice_time_ref) * t_r frame_times = np.linspace(start_time, end_time, n_scans) - design = make_design_matrix(frame_times, paradigm, + design = make_design_matrix(frame_times, events, drift_model='polynomial', drift_order=3) frame2, X2, names2 = check_design_matrix(design) assert_array_equal(frame1, frame2) @@ -261,12 +261,12 @@ def test_first_level_model_glm_computation(): # basic test based on basic_paradigm and glover hrf t_r = 1.0 slice_time_ref = 0. - paradigm = basic_paradigm() + events = basic_paradigm() # ols case model = FirstLevelModel(t_r, slice_time_ref, mask=mask, drift_model='polynomial', drift_order=3, minimize_memory=False) - model = model.fit(func_img, paradigm) + model = model.fit(func_img, events) labels1 = model.labels_[0] results1 = model.results_[0] labels2, results2 = run_glm( @@ -286,7 +286,7 @@ def test_first_level_model_contrast_computation(): # basic test based on basic_paradigm and glover hrf t_r = 1.0 slice_time_ref = 0. - paradigm = basic_paradigm() + events = basic_paradigm() # ols case model = FirstLevelModel(t_r, slice_time_ref, mask=mask, drift_model='polynomial', drift_order=3, @@ -295,7 +295,7 @@ def test_first_level_model_contrast_computation(): # asking for contrast before model fit gives error assert_raises(ValueError, model.compute_contrast, c1) # fit model - model = model.fit([func_img, func_img], [paradigm, paradigm]) + model = model.fit([func_img, func_img], [events, events]) # smoke test for different contrasts in fixed effects model.compute_contrast([c1, c2]) # smoke test for same contrast in fixed effects diff --git a/nistats/tests/test_paradigm.py b/nistats/tests/test_paradigm.py index bdeb9bd4..f9ef45de 100644 --- a/nistats/tests/test_paradigm.py +++ b/nistats/tests/test_paradigm.py @@ -16,10 +16,10 @@ def basic_paradigm(): conditions = ['c0', 'c0', 'c0', 'c1', 'c1', 'c1', 'c2', 'c2', 'c2'] onsets = [30, 70, 100, 10, 30, 90, 30, 40, 60] durations = 1 * np.ones(9) - paradigm = pd.DataFrame({'name': conditions, + events = pd.DataFrame({'name': conditions, 'onset': onsets, 'duration': durations}) - return paradigm + return events def modulated_block_paradigm(): @@ -27,11 +27,11 @@ def modulated_block_paradigm(): onsets = [30, 70, 100, 10, 30, 90, 30, 40, 60] durations = 5 + 5 * np.random.rand(len(onsets)) values = np.random.rand(len(onsets)) - paradigm = pd.DataFrame({'name': conditions, + events = pd.DataFrame({'name': conditions, 'onset': onsets, 'duration': durations, 'modulation': values}) - return paradigm + return events def modulated_event_paradigm(): @@ -39,40 +39,40 @@ def modulated_event_paradigm(): onsets = [30, 70, 100, 10, 30, 90, 30, 40, 60] durations = 1 * np.ones(9) values = np.random.rand(len(onsets)) - paradigm = pd.DataFrame({'name': conditions, + events = pd.DataFrame({'name': conditions, 'onset': onsets, 'durations': durations, 'amplitude': values}) - return paradigm + return events def block_paradigm(): conditions = ['c0', 'c0', 'c0', 'c1', 'c1', 'c1', 'c2', 'c2', 'c2'] onsets = [30, 70, 100, 10, 30, 90, 30, 40, 60] durations = 5 * np.ones(9) - paradigm = pd.DataFrame({'name': conditions, + events = pd.DataFrame({'name': conditions, 'onset': onsets, 'duration': durations}) - return paradigm + return events -def write_paradigm(paradigm, tmpdir): - """Function to write a paradigm to a file and return the address +def write_events(events, tmpdir): + """Function to write events of an experimental paradigm to a file and return the address. """ - csvfile = os.path.join(tmpdir, 'paradigm.csv') - paradigm.to_csv(csvfile) - return csvfile + tsvfile = os.path.join(tmpdir, 'events.tsv') + events.to_csv(tsvfile, sep='\t') + return tsvfile -def test_read_paradigm(): - """ test that a paradigm is correctly read +def test_read_events(): + """ test that a events for an experimental paradigm are correctly read. """ import tempfile tmpdir = tempfile.mkdtemp() - for paradigm in (block_paradigm(), + for events in (block_paradigm(), modulated_event_paradigm(), modulated_block_paradigm(), basic_paradigm()): - csvfile = write_paradigm(paradigm, tmpdir) - read_paradigm = pd.read_csv(csvfile) - assert_true((read_paradigm['onset'] == paradigm['onset']).all()) + csvfile = write_events(events, tmpdir) + read_paradigm = pd.read_table(csvfile) + assert_true((read_paradigm['onset'] == events['onset']).all()) From 6172638f8253c62055e3fde6624710ca94bd4043 Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Mon, 8 Oct 2018 14:58:45 +0200 Subject: [PATCH 156/210] Fixed circleci failure, final replacement of paradigm with events - Renaming localizer_paradigm.csv to localizer_events.tsv caused the error. The file is named so on the fetch server and hence cannot be renamed here. --- nistats/datasets.py | 2 +- nistats/tests/test_utils.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/nistats/datasets.py b/nistats/datasets.py index a6f4c3aa..76c3f804 100644 --- a/nistats/datasets.py +++ b/nistats/datasets.py @@ -327,7 +327,7 @@ def fetch_localizer_first_level(data_dir=None, verbose=1): dataset_name = "localizer_first_level" files = dict(epi_img="s12069_swaloc1_corr.nii.gz", - events="localizer_events.tsv") + events="localizer_paradigm.csv") # The options needed for _fetch_files options = [(filename, os.path.join(url, filename), {}) for _, filename in sorted(files.items())] diff --git a/nistats/tests/test_utils.py b/nistats/tests/test_utils.py index 655408e4..1d0bd76f 100644 --- a/nistats/tests/test_utils.py +++ b/nistats/tests/test_utils.py @@ -119,9 +119,9 @@ def write_fake_bold_img(file_path, shape, rk=3, affine=np.eye(4)): def basic_paradigm(): conditions = ['c0', 'c0', 'c0', 'c1', 'c1', 'c1', 'c2', 'c2', 'c2'] onsets = [30, 70, 100, 10, 30, 90, 30, 40, 60] - paradigm = pd.DataFrame({'trial_type': conditions, + events = pd.DataFrame({'trial_type': conditions, 'onset': onsets}) - return paradigm + return events def basic_confounds(length): From 7b440dd4ae992ce2f752335f744e01770b99d55a Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Mon, 8 Oct 2018 17:45:49 +0200 Subject: [PATCH 157/210] boto3/botocore is imported only for openneur fetcher --- nistats/datasets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nistats/datasets.py b/nistats/datasets.py index 69d7246a..69a31bd4 100644 --- a/nistats/datasets.py +++ b/nistats/datasets.py @@ -10,7 +10,6 @@ import re import warnings -from botocore.handlers import disable_signing import nibabel as nib import numpy as np import pandas as pd @@ -112,6 +111,7 @@ def fetch_openneuro_dataset_index( urls: list of string Sorted list of dataset directories """ + from botocore.handlers import disable_signing boto3 = _check_import_boto3("boto3") data_prefix = '{}/{}/uncompressed'.format( dataset_version.split('_')[0], dataset_version) From ef414f739183315f893d6ddfcdf1b6c93a132255 Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Mon, 8 Oct 2018 18:33:17 +0200 Subject: [PATCH 158/210] Added DeprecationWarning for python2 users --- nistats/__init__.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/nistats/__init__.py b/nistats/__init__.py index d22ad687..1f568af1 100644 --- a/nistats/__init__.py +++ b/nistats/__init__.py @@ -26,9 +26,21 @@ """ import gzip +import six +import warnings from .version import _check_module_dependencies, __version__ + +def py2_deprecation_warning(): + warnings.simplefilter('once') + py2_warning = ('Python2 support is deprecated and will be removed in ' + 'a future release. Consider switching to Python3.') + if six.PY2: + warnings.warn(message=py2_warning, category=DeprecationWarning, stacklevel=4) + _check_module_dependencies() + __all__ = ['__version__', 'datasets', 'design_matrix'] +py2_deprecation_warning() From 785ab8dcd2322067dce2dddbacd1932ccdcc9e7d Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Mon, 8 Oct 2018 18:34:01 +0200 Subject: [PATCH 159/210] Reformatted according to PEP8 line length --- nistats/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/nistats/__init__.py b/nistats/__init__.py index 1f568af1..c584ed57 100644 --- a/nistats/__init__.py +++ b/nistats/__init__.py @@ -37,7 +37,10 @@ def py2_deprecation_warning(): py2_warning = ('Python2 support is deprecated and will be removed in ' 'a future release. Consider switching to Python3.') if six.PY2: - warnings.warn(message=py2_warning, category=DeprecationWarning, stacklevel=4) + warnings.warn(message=py2_warning, + category=DeprecationWarning, + stacklevel=4, + ) _check_module_dependencies() From d2026d9039d8080cc7ba66a57e26953c82a8cb3f Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Mon, 8 Oct 2018 19:16:26 +0200 Subject: [PATCH 160/210] Added 'duration' in docstring as an essential key in events data --- nistats/design_matrix.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/nistats/design_matrix.py b/nistats/design_matrix.py index 94364642..8cae6d90 100644 --- a/nistats/design_matrix.py +++ b/nistats/design_matrix.py @@ -303,8 +303,9 @@ def make_design_matrix( 'modulation': column to specify the amplitude of each events. If missing the default is set to ones(n_events). - An experimental paradigm is valid if it has an 'onset' key. - If this key is missing an error will be raised. + An experimental paradigm is valid if it has an 'onset' key + and a 'duration' key. + If these keys are missing an error will be raised. For the others keys a warning will be displayed. Particular attention should be given to the 'trial_type' key which defines the different conditions in the experimental paradigm. From 9bfaa5d943ae4eb08bbac13236d91fe99f4b3ed3 Mon Sep 17 00:00:00 2001 From: bthirion Date: Mon, 8 Oct 2018 23:23:09 +0200 Subject: [PATCH 161/210] Added statistical tutorial --- doc/introduction.rst | 40 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/doc/introduction.rst b/doc/introduction.rst index 802f95d2..1bd08e25 100644 --- a/doc/introduction.rst +++ b/doc/introduction.rst @@ -25,6 +25,9 @@ What is nistats? A primer on BOLD-fMRI data analysis =================================== +What is fMRI ? +-------------- + Functional magnetic resonance imaging (fMRI) is based on the fact that when local neural activity increases, increases in metabolism and blood flow lead to fluctuations of the relative concentrations of oxyhemoglobin (the red cells in the blood that carry oxygen) and deoxyhemoglobin (the same red cells after they have delivered the oxygen). Because oxy- and deoxy-hemoglobin have different magnetic properties (one is diamagnetic while the other is paramagnetic), they affect the local magnetic field in different ways. The signal picked up by the MRI scanner is sensitive to these modifications of the local magnetic field. To record cerebral activity, during functional sessions, the scanner is tuned to detect this "Blood Oxygen Level Dependent" (BOLD) signal. Brain activity is measured in sessions that span several minutes, while the participant performs some a cognitive task and the scanner acquires brain images, typically every 2 or 3 seconds (the time between two successive image acquisition is called the Repetition time, or TR). @@ -35,6 +38,9 @@ A cerebral MR image provides a 3D image of the brain that can be decomposed into .. note:: A typical step in the preprocessing of MR images, involves spatially morphing these images onto a standard template (e.g. the MNI152 template from the Montreal Neurological Institute). One this is done, the coordinates of a voxel are in the same space as the template and can be used to estimate its brain location using brain atlases. As already mentioned, the nistats package is not meant to perform spatial preprocessing, but only statistical analyses on the voxel times series, regardless of the coordinate system. +FMRI data modeling +------------------ + One way to analyze times series consists in comparing them to a *model* built from our knowledge of the events that occurred during the functional session. Events can correspond to actions of the participant (e.g. button presses), presentations of sensory stimui (e.g. sound, images), or hypothesized internal processes (e.g. memorization of a stimulus), ... @@ -55,7 +61,7 @@ From the knowledge of the impulse haemodynamic response, we can build a predicte .. figure:: images/time-course-and-model-fit-in-a-voxel.png -Correlations are computed separately at each voxel and a correlation map can be produced displaying the values of correlations (real numbers between -1 and +1) at each voxel. Generally, however, the maps presented in the papers report the significance of the correlations at each voxel, in forms of T, Z or p values for the null hypothesis test of no correlation. For example, the following figure displays a Z-map showing voxels responding to auditory events. Large (positive or negative) Z values are unlikely to be due to chance alone. The map is tresholded so that only voxels with a p-value less than 1/1000 are coloured. +Correlations are computed separately at each voxel and a correlation map can be produced displaying the values of correlations (real numbers between -1 and +1) at each voxel. Generally, however, the maps presented in the papers report the significance of the correlations at each voxel, in forms of T, Z or p values for the null hypothesis test of no correlation (see below). For example, the following figure displays a Z-map showing voxels responding to auditory events. Large (positive or negative) Z values are unlikely to be due to chance alone. The map is tresholded so that only voxels with a p-value less than 1/1000 are coloured. .. note:: Because, in this approach, hypothesis tests are conducted in parallel at many voxels, the likelihood of making false alarms is important. This is known as the problem of multiple comparisons. It is beyond the scope of this short notice to explain ways in which this problem can be approached. Let us just mention that this issue can be addressed in nistats by using random permutations tests. @@ -76,11 +82,41 @@ In brief, the analysis of fMRI images involves: 4. estimate the parameters of the model, that is, the weights associated with each predictors at each voxel, using linear regression. 5. display the coefficients, or linear combination of them, and/or their statistical significance. +fMRI statistical analysis +------------------------- + +As put in the previous section, The basic statistical analysis of fMRI is conceptually a correlation analysis, where one seeks whether a certain combination (contrast) of columns of the design matrix fits a significant proportion of the fMRI signal at a given location. + +It can be shown that this is equivalent to studying whether the estimated contrast effect is large with respect to the uncertainty about its exact value. Conretely, we compute the effect size estimate and the uncertainty about its value and divide the to. The resulting number has no physical dimension, it is a statistic --- A student or t-statistic, which we will denote `t`. +Next, based on `t`, we want to decide whether the true effect was indeed greater than zero or not. + +If the true effect were zero, `t` would not necessarily be 0: by chance, the noise in the data my be partly explained by the contrast of interest. +However, if we assume that the noise is Gaussian, and that the model is correctly specificed, then we know that `t` should follow a Student distribution with `dof` degrees of freedom, where q is the number of free parameters in the model: in practive, the number of observations (i.e. the number of time points), `n_scans` minus the number of effects modeled (i.e. the number of columns `n_columns`) of the design matrix: + +:math: `dof = n_scans - n_columns` + +With this we can do statistical inference: Given a pre-defined error rate :math:`\alpha`, we compare the observed `t` to the :math:`(1-\alpha)` quantile of the Student distribution with `dof` degrees of freedom. If t is greater than this number, we can reject the null hypothesis with a *p-value* :math:`\alpha`, meaning: if there were no effect, the probability of oberving an effect as large as t would be less than `\alpha` + +.. note:: A frequent misconception consists in interpreting :math:`1-\alpha` as the probability that there is indeed an effect: this is not true ! Here we rely on a frequentist approach, that does not support Bayesian interpretation. + +.. note:: It is cumbersome to work with Student distributions, since those always require to specify the number `dof` of degrees of freedom. To avoid this, we can transform `t` to another variable `z` such that comparing `t` to the Student distribution with `dof` degrees of freedom is equivalent to comparing `z` to a standard normal distribution. We call this a z-transform of `t`. We call the :math:`(1-\alpha)` quantile of the normal distribution the *threshold*, since we use this value to declare voxels active or not. + +Multiple comparisons +-------------------- + +A well-known issue that arrives then is that of multiple comparisons: + when a statistical tests is repeated a large number times, say one for each voxel, i.e. `n_voxels` times, then one can expect that, in the absence of any effect, the number of detections ---false detections since there is no effect--- will be roughly :math:`n_voxels \alpha`. Then, take :math:`\alpha=.001` and :math:`n=10^5`, the number of false detections will be about 100. The danger is that one may no longer trust the detections, i.e. values of `z` larger than the :math:`(1-\alpha)`-quantile of the standard normal distribution. + +The first idea that one might think of is to take a much smaller :math:`\alpha`: for instance, if we take :math:`\alpha=\frac{0.05}{n\_voxels}`, then the expected number of false discoveries is only about 0.05, meaning that there is a 5% chance to declare active a truly inactive voxel. This correction on the signifiance is known as Bonferroni procedure. It is fairly accurate when the different tesst are independent or close to independent, and becomes conservative otherwise. +The problem with his approach is that truly activated voxel may not surpass the corresponding threshold, which is typically very high, because `n\_voxels` is large. + +A second possibility is to choose a threshold so that the proportion of true discoveries among the discoveries reaches a certain proportion `0`_ gallery + For tutorials, please check out the `Examples `_ gallery, especially those of the Tutorial section. .. _installation: From 6adfde108a96f0b31d4784604b99eff912589eea Mon Sep 17 00:00:00 2001 From: bthirion Date: Mon, 8 Oct 2018 23:46:38 +0200 Subject: [PATCH 162/210] More statistical stuff --- doc/images/student.png | Bin 0 -> 23472 bytes doc/introduction.rst | 15 +++++++++++---- doc/whats_new.rst | 12 +++++++++++- 3 files changed, 22 insertions(+), 5 deletions(-) create mode 100644 doc/images/student.png diff --git a/doc/images/student.png b/doc/images/student.png new file mode 100644 index 0000000000000000000000000000000000000000..67687ebd1bcb195c5d5cd1ee985c1c164617a66c GIT binary patch literal 23472 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 literal 0 HcmV?d00001 diff --git a/doc/introduction.rst b/doc/introduction.rst index 1bd08e25..7ce04647 100644 --- a/doc/introduction.rst +++ b/doc/introduction.rst @@ -95,23 +95,30 @@ However, if we assume that the noise is Gaussian, and that the model is correctl :math: `dof = n_scans - n_columns` -With this we can do statistical inference: Given a pre-defined error rate :math:`\alpha`, we compare the observed `t` to the :math:`(1-\alpha)` quantile of the Student distribution with `dof` degrees of freedom. If t is greater than this number, we can reject the null hypothesis with a *p-value* :math:`\alpha`, meaning: if there were no effect, the probability of oberving an effect as large as t would be less than `\alpha` +With this we can do statistical inference: Given a pre-defined error rate :math:`\alpha`, we compare the observed `t` to the :math:`(1-\alpha)` quantile of the Student distribution with `dof` degrees of freedom. If t is greater than this number, we can reject the null hypothesis with a *p-value* :math:`\alpha`, meaning: if there were no effect, the probability of oberving an effect as large as t would be less than `\alpha`. -.. note:: A frequent misconception consists in interpreting :math:`1-\alpha` as the probability that there is indeed an effect: this is not true ! Here we rely on a frequentist approach, that does not support Bayesian interpretation. +.. figure:: images/student.png +.. note:: A frequent misconception consists in interpreting :math:`1-\alpha` as the probability that there is indeed an effect: this is not true ! Here we rely on a frequentist approach, that does not support Bayesian interpretation. See e.g. https://en.wikipedia.org/wiki/Frequentist_inference + .. note:: It is cumbersome to work with Student distributions, since those always require to specify the number `dof` of degrees of freedom. To avoid this, we can transform `t` to another variable `z` such that comparing `t` to the Student distribution with `dof` degrees of freedom is equivalent to comparing `z` to a standard normal distribution. We call this a z-transform of `t`. We call the :math:`(1-\alpha)` quantile of the normal distribution the *threshold*, since we use this value to declare voxels active or not. Multiple comparisons -------------------- A well-known issue that arrives then is that of multiple comparisons: - when a statistical tests is repeated a large number times, say one for each voxel, i.e. `n_voxels` times, then one can expect that, in the absence of any effect, the number of detections ---false detections since there is no effect--- will be roughly :math:`n_voxels \alpha`. Then, take :math:`\alpha=.001` and :math:`n=10^5`, the number of false detections will be about 100. The danger is that one may no longer trust the detections, i.e. values of `z` larger than the :math:`(1-\alpha)`-quantile of the standard normal distribution. + when a statistical tests is repeated a large number times, say one for each voxel, i.e. `n_voxels` times, then one can expect that, in the absence of any effect, the number of detections ---false detections since there is no effect--- will be roughly :math:`n\_voxels \alpha`. Then, take :math:`\alpha=.001` and :math:`n=10^5`, the number of false detections will be about 100. The danger is that one may no longer trust the detections, i.e. values of `z` larger than the :math:`(1-\alpha)`-quantile of the standard normal distribution. -The first idea that one might think of is to take a much smaller :math:`\alpha`: for instance, if we take :math:`\alpha=\frac{0.05}{n\_voxels}`, then the expected number of false discoveries is only about 0.05, meaning that there is a 5% chance to declare active a truly inactive voxel. This correction on the signifiance is known as Bonferroni procedure. It is fairly accurate when the different tesst are independent or close to independent, and becomes conservative otherwise. +The first idea that one might think of is to take a much smaller :math:`\alpha`: for instance, if we take, :math:`\alpha=\frac{0.05}{n\_voxels}` then the expected number of false discoveries is only about 0.05, meaning that there is a 5% chance to declare active a truly inactive voxel. This correction on the signifiance is known as Bonferroni procedure. It is fairly accurate when the different tesst are independent or close to independent, and becomes conservative otherwise. The problem with his approach is that truly activated voxel may not surpass the corresponding threshold, which is typically very high, because `n\_voxels` is large. A second possibility is to choose a threshold so that the proportion of true discoveries among the discoveries reaches a certain proportion `0= 0.18.0 * third argument of map_threshold is now called 'level'. - + +* Added comprehensive tutorial + +* changed the term `paradigm` to `events` and made it + BIDS-compliant. Set the event file to be tab-separated + +* Changed the defaut oversampling value for the hemodynamic response + to 50 and exposed this parameter. + +* Second-level model accepts 4D images as input. + 0.0.1a ======= From e2d00acaebdd15f7465923d620b6b920a0ffe0c6 Mon Sep 17 00:00:00 2001 From: bthirion Date: Tue, 9 Oct 2018 00:36:19 +0200 Subject: [PATCH 163/210] Fixed erroneous tests --- nistats/tests/test_first_level_model.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nistats/tests/test_first_level_model.py b/nistats/tests/test_first_level_model.py index 6fd42cd5..de03f4d3 100644 --- a/nistats/tests/test_first_level_model.py +++ b/nistats/tests/test_first_level_model.py @@ -232,7 +232,7 @@ def test_first_level_model_design_creation(): FUNCFILE = FUNCFILE[0] func_img = load(FUNCFILE) # basic test based on basic_paradigm and glover hrf - t_r = 1.0 + t_r = 10.0 slice_time_ref = 0. events = basic_paradigm() model = FirstLevelModel(t_r, slice_time_ref, mask=mask, @@ -259,7 +259,7 @@ def test_first_level_model_glm_computation(): FUNCFILE = FUNCFILE[0] func_img = load(FUNCFILE) # basic test based on basic_paradigm and glover hrf - t_r = 1.0 + t_r = 10.0 slice_time_ref = 0. events = basic_paradigm() # ols case @@ -284,7 +284,7 @@ def test_first_level_model_contrast_computation(): FUNCFILE = FUNCFILE[0] func_img = load(FUNCFILE) # basic test based on basic_paradigm and glover hrf - t_r = 1.0 + t_r = 10.0 slice_time_ref = 0. events = basic_paradigm() # ols case From 8d93e437374889d832f75cc73b88fb2fa5890262 Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Tue, 9 Oct 2018 01:13:52 +0200 Subject: [PATCH 164/210] Renamed functon names to use more consistent, less redundant voabulary - nistats.design_matrix.make_design_matrix -> make_first_level_design_matrix - nistats.design_matrix.create_second_level_design -> make_second_level_design_matrix - nistats.utils.pos_recipr -> positive_reciprocal. - nistats.utils.multiple_fast_inv -> nistats.utils.multiple_fast_inverse --- doc/modules/reference.rst | 8 ++-- .../02_first_level_models/plot_adhd_dmn.py | 8 ++-- .../plot_localizer_surface_analysis.py | 10 ++--- .../plot_spm_multimodal_faces.py | 4 +- .../plot_design_matrix.py | 14 +++---- .../plot_second_level_design_matrix.py | 4 +- nistats/design_matrix.py | 6 +-- nistats/first_level_model.py | 12 +++--- nistats/model.py | 10 ++--- nistats/regression.py | 6 +-- nistats/second_level_model.py | 6 +-- nistats/tests/test_dmtx.py | 42 +++++++++---------- nistats/tests/test_first_level_model.py | 6 +-- nistats/tests/test_reporting.py | 6 +-- nistats/tests/test_utils.py | 18 ++++---- nistats/utils.py | 6 +-- 16 files changed, 83 insertions(+), 83 deletions(-) diff --git a/doc/modules/reference.rst b/doc/modules/reference.rst index 4f5b671e..efbc480d 100644 --- a/doc/modules/reference.rst +++ b/doc/modules/reference.rst @@ -78,9 +78,9 @@ uses. :toctree: generated/ :template: function.rst - make_design_matrix + make_first_level_design_matrix check_design_matrix - create_second_level_design + make_second_level_design_matrix .. _experimental_paradigm_ref: @@ -283,9 +283,9 @@ uses. :template: function.rst z_score - multiple_fast_inv + multiple_fast_inverse multiple_mahalanobis full_rank - pos_recipr + positive_reciprocal get_bids_files parse_bids_filename diff --git a/examples/02_first_level_models/plot_adhd_dmn.py b/examples/02_first_level_models/plot_adhd_dmn.py index e6fbef62..20d8739f 100644 --- a/examples/02_first_level_models/plot_adhd_dmn.py +++ b/examples/02_first_level_models/plot_adhd_dmn.py @@ -20,7 +20,7 @@ from nilearn.input_data import NiftiSpheresMasker from nistats.first_level_model import FirstLevelModel -from nistats.design_matrix import make_design_matrix +from nistats.design_matrix import make_first_level_design_matrix ######################################################################### # Prepare data and analysis parameters @@ -47,9 +47,9 @@ memory_level=1, verbose=0) seed_time_series = seed_masker.fit_transform(adhd_dataset.func[0]) frametimes = np.linspace(0, (n_scans - 1) * t_r, n_scans) -design_matrix = make_design_matrix(frametimes, hrf_model='spm', - add_regs=seed_time_series, - add_reg_names=["pcc_seed"]) +design_matrix = make_first_level_design_matrix(frametimes, hrf_model='spm', + add_regs=seed_time_series, + add_reg_names=["pcc_seed"]) dmn_contrast = np.array([1] + [0]*(design_matrix.shape[1]-1)) contrasts = {'seed_based_glm': dmn_contrast} diff --git a/examples/02_first_level_models/plot_localizer_surface_analysis.py b/examples/02_first_level_models/plot_localizer_surface_analysis.py index 8e0b8642..af702401 100644 --- a/examples/02_first_level_models/plot_localizer_surface_analysis.py +++ b/examples/02_first_level_models/plot_localizer_surface_analysis.py @@ -85,11 +85,11 @@ # # We specify an hrf model containing Glover model and its time derivative # the drift model is implicitly a cosine basis with period cutoff 128s. -from nistats.design_matrix import make_design_matrix -design_matrix = make_design_matrix(frame_times, - events=events, - hrf_model='glover + derivative' - ) +from nistats.design_matrix import make_first_level_design_matrix +design_matrix = make_first_level_design_matrix(frame_times, + events=events, + hrf_model='glover + derivative' + ) ######################################################################### # Setup and fit GLM. diff --git a/examples/02_first_level_models/plot_spm_multimodal_faces.py b/examples/02_first_level_models/plot_spm_multimodal_faces.py index 354b820d..8b9de1b2 100644 --- a/examples/02_first_level_models/plot_spm_multimodal_faces.py +++ b/examples/02_first_level_models/plot_spm_multimodal_faces.py @@ -55,7 +55,7 @@ # Make design matrices import numpy as np import pandas as pd -from nistats.design_matrix import make_design_matrix +from nistats.design_matrix import make_first_level_design_matrix design_matrices = [] ######################################################################### @@ -67,7 +67,7 @@ # Define the sampling times for the design matrix frame_times = np.arange(n_scans) * tr # Build design matrix with the reviously defined parameters - design_matrix = make_design_matrix( + design_matrix = make_first_level_design_matrix( frame_times, events, hrf_model=hrf_model, diff --git a/examples/04_low_level_functions/plot_design_matrix.py b/examples/04_low_level_functions/plot_design_matrix.py index 49448561..b6db7373 100644 --- a/examples/04_low_level_functions/plot_design_matrix.py +++ b/examples/04_low_level_functions/plot_design_matrix.py @@ -50,8 +50,8 @@ ######################################################################### # We sample the events into a design matrix, also including additional regressors hrf_model = 'glover' -from nistats.design_matrix import make_design_matrix -X1 = make_design_matrix( +from nistats.design_matrix import make_first_level_design_matrix +X1 = make_first_level_design_matrix( frame_times, events, drift_model='polynomial', drift_order=3, add_regs=motion, add_reg_names=add_reg_names, hrf_model=hrf_model) @@ -64,17 +64,17 @@ ######################################################################### # Then we sample the design matrix -X2 = make_design_matrix(frame_times, events, drift_model='polynomial', - drift_order=3, hrf_model=hrf_model) +X2 = make_first_level_design_matrix(frame_times, events, drift_model='polynomial', + drift_order=3, hrf_model=hrf_model) ######################################################################### # Finally we compute a FIR model events = pd.DataFrame({'trial_type': conditions, 'onset': onsets, 'duration': duration}) hrf_model = 'FIR' -X3 = make_design_matrix(frame_times, events, hrf_model='fir', - drift_model='polynomial', drift_order=3, - fir_delays=np.arange(1, 6)) +X3 = make_first_level_design_matrix(frame_times, events, hrf_model='fir', + drift_model='polynomial', drift_order=3, + fir_delays=np.arange(1, 6)) ######################################################################### # Here the three designs side by side diff --git a/examples/04_low_level_functions/plot_second_level_design_matrix.py b/examples/04_low_level_functions/plot_second_level_design_matrix.py index 628a9037..38b00a84 100644 --- a/examples/04_low_level_functions/plot_second_level_design_matrix.py +++ b/examples/04_low_level_functions/plot_second_level_design_matrix.py @@ -36,8 +36,8 @@ ######################################################################### # Create a second level design matrix # ----------------------------------- -from nistats.design_matrix import create_second_level_design -design_matrix = create_second_level_design(subjects_label, extra_info_subjects) +from nistats.design_matrix import make_second_level_design_matrix +design_matrix = make_second_level_design_matrix(subjects_label, extra_info_subjects) ######################################################################### # plot the results diff --git a/nistats/design_matrix.py b/nistats/design_matrix.py index 8cae6d90..75164ef6 100644 --- a/nistats/design_matrix.py +++ b/nistats/design_matrix.py @@ -5,7 +5,7 @@ Design matrices are represented by Pandas DataFrames Computations of the different parts of the design matrix are confined -to the make_design_matrix function, that create a DataFrame +to the make_first_level_design_matrix function, that create a DataFrame All the others are ancillary functions. Design matrices contain three different types of regressors: @@ -278,7 +278,7 @@ def _full_rank(X, cmax=1e15): ###################################################################### -def make_design_matrix( +def make_first_level_design_matrix( frame_times, events=None, hrf_model='glover', drift_model='cosine', period_cut=128, drift_order=1, fir_delays=[0], add_regs=None, add_reg_names=None, min_onset=-24, oversampling=50): @@ -441,7 +441,7 @@ def check_design_matrix(design_matrix): return frame_times, matrix, names -def create_second_level_design(subjects_label, confounds=None): +def make_second_level_design_matrix(subjects_label, confounds=None): """Sets up a second level design. Construct a design matrix with an intercept and subject specific confounds. diff --git a/nistats/first_level_model.py b/nistats/first_level_model.py index ca92f1e8..767bede8 100644 --- a/nistats/first_level_model.py +++ b/nistats/first_level_model.py @@ -31,7 +31,7 @@ from patsy import DesignInfo from .regression import OLSModel, ARModel, SimpleRegressionResults -from .design_matrix import make_design_matrix +from .design_matrix import make_first_level_design_matrix from .contrasts import _fixed_effect_contrast from .utils import (_basestring, _check_run_tables, _verify_events_file_uses_tab_separators, @@ -434,11 +434,11 @@ def fit(self, run_imgs, events=None, confounds=None, start_time = self.slice_time_ref * self.t_r end_time = (n_scans - 1 + self.slice_time_ref) * self.t_r frame_times = np.linspace(start_time, end_time, n_scans) - design = make_design_matrix(frame_times, events[run_idx], - self.hrf_model, self.drift_model, - self.period_cut, self.drift_order, - self.fir_delays, confounds_matrix, - confounds_names, self.min_onset) + design = make_first_level_design_matrix(frame_times, events[run_idx], + self.hrf_model, self.drift_model, + self.period_cut, self.drift_order, + self.fir_delays, confounds_matrix, + confounds_names, self.min_onset) else: design = design_matrices[run_idx] self.design_matrices_.append(design) diff --git a/nistats/model.py b/nistats/model.py index eb33ad85..140e507d 100644 --- a/nistats/model.py +++ b/nistats/model.py @@ -12,7 +12,7 @@ from nibabel.onetime import setattr_on_read -from .utils import pos_recipr +from .utils import positive_reciprocal # Inverse t cumulative distribution inv_t_cdf = t_distribution.ppf @@ -98,7 +98,7 @@ def t(self, column=None): _cov = self.vcov(column=column) if _cov.ndim == 2: _cov = np.diag(_cov) - _t = _theta * pos_recipr(np.sqrt(_cov)) + _t = _theta * positive_reciprocal(np.sqrt(_cov)) return _t def vcov(self, matrix=None, column=None, dispersion=None, other=None): @@ -200,7 +200,7 @@ def Tcontrast(self, matrix, store=('t', 'effect', 'sd'), dispersion=None): if 'sd' in store: st_sd = np.squeeze(sd) if 't' in store: - st_t = np.squeeze(effect * pos_recipr(sd)) + st_t = np.squeeze(effect * positive_reciprocal(sd)) return TContrastResults(effect=st_effect, t=st_t, sd=st_sd, df_den=self.df_resid) @@ -258,8 +258,8 @@ def Fcontrast(self, matrix, dispersion=None, invcov=None): q = matrix.shape[0] if invcov is None: invcov = inv(self.vcov(matrix=matrix, dispersion=1.0)) - F = np.add.reduce(np.dot(invcov, ctheta) * ctheta, 0) *\ - pos_recipr((q * dispersion)) + F = np.add.reduce(np.dot(invcov, ctheta) * ctheta, 0) * \ + positive_reciprocal((q * dispersion)) F = np.squeeze(F) return FContrastResults( effect=ctheta, covariance=self.vcov( diff --git a/nistats/regression.py b/nistats/regression.py index 081fdffc..6ae048b8 100644 --- a/nistats/regression.py +++ b/nistats/regression.py @@ -29,7 +29,7 @@ from nibabel.onetime import setattr_on_read -from .utils import pos_recipr +from .utils import positive_reciprocal from .model import LikelihoodModelResults @@ -308,7 +308,7 @@ def norm_resid(self): See: Montgomery and Peck 3.2.1 p. 68 Davidson and MacKinnon 15.2 p 662 """ - return self.resid * pos_recipr(np.sqrt(self.dispersion)) + return self.resid * positive_reciprocal(np.sqrt(self.dispersion)) @setattr_on_read def predicted(self): @@ -382,7 +382,7 @@ def norm_resid(self, Y): See: Montgomery and Peck 3.2.1 p. 68 Davidson and MacKinnon 15.2 p 662 """ - return self.resid(Y) * pos_recipr(np.sqrt(self.dispersion)) + return self.resid(Y) * positive_reciprocal(np.sqrt(self.dispersion)) def predicted(self): """ Return linear predictor values from a design matrix. diff --git a/nistats/second_level_model.py b/nistats/second_level_model.py index 7993ffb4..aa1996f0 100644 --- a/nistats/second_level_model.py +++ b/nistats/second_level_model.py @@ -27,7 +27,7 @@ from .regression import SimpleRegressionResults from .contrasts import compute_contrast from .utils import _basestring -from .design_matrix import create_second_level_design +from .design_matrix import make_second_level_design_matrix def _infer_effect_maps(second_level_input, contrast_def): @@ -284,8 +284,8 @@ def fit(self, second_level_input, confounds=None, design_matrix=None): # Create and set design matrix, if not given if design_matrix is None: - design_matrix = create_second_level_design(subjects_label, - confounds) + design_matrix = make_second_level_design_matrix(subjects_label, + confounds) self.design_matrix_ = design_matrix # Learn the mask. Assume the first level imgs have been masked. diff --git a/nistats/tests/test_dmtx.py b/nistats/tests/test_dmtx.py index 42581339..9133182e 100644 --- a/nistats/tests/test_dmtx.py +++ b/nistats/tests/test_dmtx.py @@ -13,9 +13,9 @@ from nilearn._utils.testing import assert_raises_regex from nistats.design_matrix import ( - _convolve_regressors, make_design_matrix, + _convolve_regressors, make_first_level_design_matrix, _cosine_drift, check_design_matrix, - create_second_level_design) + make_second_level_design_matrix) from nibabel.tmpdirs import InTemporaryDirectory @@ -33,11 +33,11 @@ def design_matrix_light( frame_times, events=None, hrf_model='glover', drift_model='cosine', period_cut=128, drift_order=1, fir_delays=[0], add_regs=None, add_reg_names=None, min_onset=-24, path=None): - """ Idem make_design_matrix, but only returns the computed matrix + """ Idem make_first_level_design_matrix, but only returns the computed matrix and associated names """ - dmtx = make_design_matrix(frame_times, events, hrf_model, - drift_model, period_cut, drift_order, fir_delays, - add_regs, add_reg_names, min_onset) + dmtx = make_first_level_design_matrix(frame_times, events, hrf_model, + drift_model, period_cut, drift_order, fir_delays, + add_regs, add_reg_names, min_onset) _, matrix, names = check_design_matrix(dmtx) return matrix, names @@ -101,7 +101,7 @@ def test_design_matrix0(): # Test design matrix creation when no experimental paradigm is provided tr = 1.0 frame_times = np.linspace(0, 127 * tr, 128) - _, X, names = check_design_matrix(make_design_matrix( + _, X, names = check_design_matrix(make_first_level_design_matrix( frame_times, drift_model='polynomial', drift_order=3)) assert_equal(len(names), 4) x = np.linspace(- 0.5, .5, 128) @@ -113,7 +113,7 @@ def test_design_matrix0c(): tr = 1.0 frame_times = np.linspace(0, 127 * tr, 128) ax = np.random.randn(128, 4) - _, X, names = check_design_matrix(make_design_matrix( + _, X, names = check_design_matrix(make_first_level_design_matrix( frame_times, drift_model='polynomial', drift_order=3, add_regs=ax)) assert_almost_equal(X[:, 0], ax[:, 0]) @@ -121,12 +121,12 @@ def test_design_matrix0c(): assert_raises_regex( AssertionError, "Incorrect specification of additional regressors:.", - make_design_matrix, frame_times, add_regs=ax) + make_first_level_design_matrix, frame_times, add_regs=ax) ax = np.random.randn(128, 4) assert_raises_regex( ValueError, "Incorrect number of additional regressor names.", - make_design_matrix, frame_times, add_regs=ax, add_reg_names='') + make_first_level_design_matrix, frame_times, add_regs=ax, add_reg_names='') def test_design_matrix0d(): @@ -134,7 +134,7 @@ def test_design_matrix0d(): tr = 1.0 frame_times = np.linspace(0, 127 * tr, 128) ax = np.random.randn(128, 4) - _, X, names = check_design_matrix(make_design_matrix( + _, X, names = check_design_matrix(make_first_level_design_matrix( frame_times, drift_model='polynomial', drift_order=3, add_regs=ax)) assert_equal(len(names), 8) assert_equal(X.shape[1], 8) @@ -431,11 +431,11 @@ def test_fir_block(): def test_oversampling(): events = basic_paradigm() frame_times = np.linspace(0, 127, 128) - X1 = make_design_matrix( + X1 = make_first_level_design_matrix( frame_times, events, drift_model=None) - X2 = make_design_matrix( + X2 = make_first_level_design_matrix( frame_times, events, drift_model=None, oversampling=50) - X3 = make_design_matrix( + X3 = make_first_level_design_matrix( frame_times, events, drift_model=None, oversampling=10) # oversampling = 16 is the default so X2 = X1, X3 \neq X1, X3 close to X2 @@ -444,10 +444,10 @@ def test_oversampling(): assert_true(np.linalg.norm(X2.values - X3.values) / np.linalg.norm(X2.values) > 1.e-4) # fir model, oversampling is forced to 1 - X4 = make_design_matrix( + X4 = make_first_level_design_matrix( frame_times, events, hrf_model='fir', drift_model=None, fir_delays=range(0, 4), oversampling=1) - X5 = make_design_matrix( + X5 = make_first_level_design_matrix( frame_times, events, hrf_model='fir', drift_model=None, fir_delays=range(0, 4), oversampling=3) assert_almost_equal(X4.values, X5.values) @@ -457,8 +457,8 @@ def test_csv_io(): tr = 1.0 frame_times = np.linspace(0, 127 * tr, 128) events = modulated_event_paradigm() - DM = make_design_matrix(frame_times, events, hrf_model='glover', - drift_model='polynomial', drift_order=3) + DM = make_first_level_design_matrix(frame_times, events, hrf_model='glover', + drift_model='polynomial', drift_order=3) path = 'design_matrix.csv' with InTemporaryDirectory(): DM.to_csv(path) @@ -480,7 +480,7 @@ def test_spm_1(): events = pd.DataFrame({'trial_type': conditions, 'onset': onsets, 'duration': durations}) - X1 = make_design_matrix(frame_times, events, drift_model=None) + X1 = make_first_level_design_matrix(frame_times, events, drift_model=None) _, matrix, _ = check_design_matrix(X1) spm_design_matrix = DESIGN_MATRIX['arr_0'] assert_true(((spm_design_matrix - matrix) ** 2).sum() / @@ -497,7 +497,7 @@ def test_spm_2(): events = pd.DataFrame({'trial_type': conditions, 'onset': onsets, 'duration': durations}) - X1 = make_design_matrix(frame_times, events, drift_model=None) + X1 = make_first_level_design_matrix(frame_times, events, drift_model=None) spm_design_matrix = DESIGN_MATRIX['arr_1'] _, matrix, _ = check_design_matrix(X1) assert_true(((spm_design_matrix - matrix) ** 2).sum() / @@ -519,7 +519,7 @@ def test_create_second_level_design(): subjects_label = ['02', '01'] # change order to test right output order regressors = [['01', 0.1], ['02', 0.75]] regressors = pd.DataFrame(regressors, columns=['subject_label', 'f1']) - design = create_second_level_design(subjects_label, regressors) + design = make_second_level_design_matrix(subjects_label, regressors) expected_design = np.array([[0.75, 1], [0.1, 1]]) assert_array_equal(design, expected_design) assert_true(len(design.columns) == 2) diff --git a/nistats/tests/test_first_level_model.py b/nistats/tests/test_first_level_model.py index 6fd42cd5..5d9fafdd 100644 --- a/nistats/tests/test_first_level_model.py +++ b/nistats/tests/test_first_level_model.py @@ -13,7 +13,7 @@ from nistats.first_level_model import (mean_scaling, run_glm, FirstLevelModel, first_level_models_from_bids) -from nistats.design_matrix import check_design_matrix, make_design_matrix +from nistats.design_matrix import check_design_matrix, make_first_level_design_matrix from nose.tools import assert_true, assert_equal, assert_raises from numpy.testing import (assert_almost_equal, assert_array_equal) @@ -244,8 +244,8 @@ def test_first_level_model_design_creation(): start_time = slice_time_ref * t_r end_time = (n_scans - 1 + slice_time_ref) * t_r frame_times = np.linspace(start_time, end_time, n_scans) - design = make_design_matrix(frame_times, events, - drift_model='polynomial', drift_order=3) + design = make_first_level_design_matrix(frame_times, events, + drift_model='polynomial', drift_order=3) frame2, X2, names2 = check_design_matrix(design) assert_array_equal(frame1, frame2) assert_array_equal(X1, X2) diff --git a/nistats/tests/test_reporting.py b/nistats/tests/test_reporting.py index de513d90..b0f970a6 100644 --- a/nistats/tests/test_reporting.py +++ b/nistats/tests/test_reporting.py @@ -5,7 +5,7 @@ from nose.tools import assert_true from nibabel.tmpdirs import InTemporaryDirectory -from nistats.design_matrix import make_design_matrix +from nistats.design_matrix import make_first_level_design_matrix from nistats.reporting import (plot_design_matrix, get_clusters_table, _local_max, plot_contrast_matrix) @@ -27,7 +27,7 @@ def test_show_design_matrix(): # test that the show code indeed (formally) runs frame_times = np.linspace(0, 127 * 1., 128) - dmtx = make_design_matrix( + dmtx = make_first_level_design_matrix( frame_times, drift_model='polynomial', drift_order=3) ax = plot_design_matrix(dmtx) assert (ax is not None) @@ -42,7 +42,7 @@ def test_show_design_matrix(): def test_show_contrast_matrix(): # test that the show code indeed (formally) runs frame_times = np.linspace(0, 127 * 1., 128) - dmtx = make_design_matrix( + dmtx = make_first_level_design_matrix( frame_times, drift_model='polynomial', drift_order=3) contrast = np.ones(4) ax = plot_contrast_matrix(contrast, dmtx) diff --git a/nistats/tests/test_utils.py b/nistats/tests/test_utils.py index 1d0bd76f..fa86b1b6 100644 --- a/nistats/tests/test_utils.py +++ b/nistats/tests/test_utils.py @@ -11,8 +11,8 @@ from nibabel.tmpdirs import InTemporaryDirectory from nose import with_setup -from nistats.utils import (multiple_mahalanobis, z_score, multiple_fast_inv, - pos_recipr, full_rank, _check_run_tables, +from nistats.utils import (multiple_mahalanobis, z_score, multiple_fast_inverse, + positive_reciprocal, full_rank, _check_run_tables, _check_and_load_tables, _check_list_length_match, get_bids_files, parse_bids_filename, get_design_from_fslmat) @@ -69,26 +69,26 @@ def test_multiple_fast_inv(): for i in range(shape[0]): X[i] = np.dot(X[i], X[i].T) X_inv_ref[i] = spl.inv(X[i]) - X_inv = multiple_fast_inv(X) + X_inv = multiple_fast_inverse(X) assert_almost_equal(X_inv_ref, X_inv) def test_pos_recipr(): X = np.array([2, 1, -1, 0], dtype=np.int8) eX = np.array([0.5, 1, 0, 0]) - Y = pos_recipr(X) + Y = positive_reciprocal(X) yield assert_array_almost_equal, Y, eX yield assert_equal, Y.dtype.type, np.float64 X2 = X.reshape((2, 2)) - Y2 = pos_recipr(X2) + Y2 = positive_reciprocal(X2) yield assert_array_almost_equal, Y2, eX.reshape((2, 2)) # check that lists have arrived XL = [0, 1, -1] - yield assert_array_almost_equal, pos_recipr(XL), [0, 1, 0] + yield assert_array_almost_equal, positive_reciprocal(XL), [0, 1, 0] # scalars - yield assert_equal, pos_recipr(-1), 0 - yield assert_equal, pos_recipr(0), 0 - yield assert_equal, pos_recipr(2), 0.5 + yield assert_equal, positive_reciprocal(-1), 0 + yield assert_equal, positive_reciprocal(0), 0 + yield assert_equal, positive_reciprocal(2), 0.5 def test_img_table_checks(): diff --git a/nistats/utils.py b/nistats/utils.py index 249bc65a..50105c7a 100644 --- a/nistats/utils.py +++ b/nistats/utils.py @@ -137,7 +137,7 @@ def z_score(pvalue): return norm.isf(pvalue) -def multiple_fast_inv(a): +def multiple_fast_inverse(a): """Compute the inverse of a set of arrays. Parameters @@ -226,7 +226,7 @@ def multiple_mahalanobis(effect, covariance): Xt, Kt = np.ascontiguousarray(effect.T), np.ascontiguousarray(covariance.T) # compute the inverse of the covariances - Kt = multiple_fast_inv(Kt) + Kt = multiple_fast_inverse(Kt) # derive the squared Mahalanobis distances sqd = np.sum(np.sum(Xt[:, :, np.newaxis] * Xt[:, np.newaxis] * Kt, 1), 1) @@ -265,7 +265,7 @@ def full_rank(X, cmax=1e15): return X, cmax -def pos_recipr(X): +def positive_reciprocal(X): """ Return element-wise reciprocal of array, setting `X`>=0 to 0 Return the reciprocal of an array, setting all entries less than or From 58e5f3708bac069bede69cce0c72026606b44bbb Mon Sep 17 00:00:00 2001 From: bthirion Date: Tue, 9 Oct 2018 11:25:53 +0200 Subject: [PATCH 165/210] updated preprocessing paragraph --- doc/introduction.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/introduction.rst b/doc/introduction.rst index 7ce04647..6cbd23a3 100644 --- a/doc/introduction.rst +++ b/doc/introduction.rst @@ -36,7 +36,7 @@ A cerebral MR image provides a 3D image of the brain that can be decomposed into .. _a voxels: https://en.wikipedia.org/wiki/Voxel -.. note:: A typical step in the preprocessing of MR images, involves spatially morphing these images onto a standard template (e.g. the MNI152 template from the Montreal Neurological Institute). One this is done, the coordinates of a voxel are in the same space as the template and can be used to estimate its brain location using brain atlases. As already mentioned, the nistats package is not meant to perform spatial preprocessing, but only statistical analyses on the voxel times series, regardless of the coordinate system. +.. note:: Before fMRI images can be used to do meaningful comparisons, they must be processed to ensure that the voxels that are being compared represent the same brain regions, irrespective of the variability in size and shape of the brain and its microarchitecture across different subjects in the experiment. The process is called spatial registration or spatial normalization. During this procedure, the voxels of all the brain images are 'registered' to correspond to the same region of the brain. Usually, the images (their voxels) are registered to a standard 'template' brain image (its voxels) . One often used standard template is the MNI152 template from the Montreal Neurological Institute. One this is done, the coordinates of a voxel are in the same space as the template and can be used to estimate its brain location using brain atlases based on tht same template.As already mentioned, the nistats package is not meant to perform spatial preprocessing, but only statistical analyses on the voxel times series, regardless of the coordinate system. FMRI data modeling ------------------ From 036c2087a8054dff6b4c536b53eec27f1fe892e1 Mon Sep 17 00:00:00 2001 From: bthirion Date: Tue, 9 Oct 2018 13:38:30 +0200 Subject: [PATCH 166/210] further typos --- doc/introduction.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/introduction.rst b/doc/introduction.rst index 6cbd23a3..b2d13eb2 100644 --- a/doc/introduction.rst +++ b/doc/introduction.rst @@ -32,7 +32,7 @@ Functional magnetic resonance imaging (fMRI) is based on the fact that when loca Brain activity is measured in sessions that span several minutes, while the participant performs some a cognitive task and the scanner acquires brain images, typically every 2 or 3 seconds (the time between two successive image acquisition is called the Repetition time, or TR). -A cerebral MR image provides a 3D image of the brain that can be decomposed into `a voxels`_ (the equivalent of pixels, but in 3 dimensions). The series of images acquired during a functional session provides, in each voxel, a time series of positive real number representing the MRI signal, sampled at the TR. +A cerebral MR image provides a 3D image of the brain that can be decomposed into `voxels`_ (the equivalent of pixels, but in 3 dimensions). The series of images acquired during a functional session provides, in each voxel, a time series of positive real number representing the MRI signal, sampled at the TR. .. _a voxels: https://en.wikipedia.org/wiki/Voxel @@ -49,7 +49,7 @@ One way to analyze times series consists in comparing them to a *model* built fr One expects that a brain region involved in the processing of a certain type of event (e.g. the auditory cortex for sounds), would show a time course of activation that correlates with the time-diagram of these events. If the fMRI signal directly showed neural activity and did not contain any noise, we could just look at it in various voxel and detect those that conform to the time-diagrams. -Yet, we know, from previous measurements, that the BOLD signal does not follow the exact time course of stimulus processing and the underlying neural activity. The BOLD response reflects changes in blood flow and concentrations in oxy-deoxy hemoglobin, all together forming an `a haemodynamic response`_ which is sluggish and long-lasting, as can be seen on the following figure showing the response to an impulsive event (for example, an auditory click played to the participants). +Yet, we know, from previous measurements, that the BOLD signal does not follow the exact time course of stimulus processing and the underlying neural activity. The BOLD response reflects changes in blood flow and concentrations in oxy-deoxy hemoglobin, all together forming an `haemodynamic response`_ which is sluggish and long-lasting, as can be seen on the following figure showing the response to an impulsive event (for example, an auditory click played to the participants). .. figure:: images/spm_iHRF.png From 4514a7c03e9b6e89f78c1c79a09038d6c3d12bbd Mon Sep 17 00:00:00 2001 From: bthirion Date: Tue, 9 Oct 2018 13:53:24 +0200 Subject: [PATCH 167/210] Some cleaning --- .../plot_first_level_model_details.py | 128 +++++++++++++----- 1 file changed, 97 insertions(+), 31 deletions(-) diff --git a/examples/01_tutorials/plot_first_level_model_details.py b/examples/01_tutorials/plot_first_level_model_details.py index 541afd3e..59478482 100644 --- a/examples/01_tutorials/plot_first_level_model_details.py +++ b/examples/01_tutorials/plot_first_level_model_details.py @@ -28,24 +28,47 @@ # # We use a so-called localizer dataset, which consists in a 5-minutes # acquisition of a fast event-related dataset. +# from nistats import datasets data = datasets.fetch_localizer_first_level() fmri_img = data.epi_img ############################################################################### -# Define the experimental events that will be used +# Define the paradigm that will be used. Here, we just need to get the provided file. +# +# This task, described in Pinel et al., BMC neuroscience 2007 probes +# basic functions, such as button with the left or right hand, viewing +# horizontal and vertical checkerboards, reading and listening to +# short sentences, and mental computations (subractions). +# +# Visual stimuli were displayed in four 250-ms epochs, separated by +# 100ms intervals (i.e., 1.3s in total). Auditory stimuli were drawn +# from a recorded male voice (i.e., a total of 1.6s for motor +# instructions, 1.2-1.7s for sentences, and 1.2-1.3s for +# subtraction). The auditory or visual stimuli were shown to the +# participants for passive viewing or button response in +# event-related paradigms. Post-scan questions verified that the +# experimental tasks were understood and followed correctly. +# +# This task comprises 10 conditions: +# +# * clicGaudio: Left-hand three-times button press, indicated by visual instruction +# * clicDaudio: Right-hand three-times button press, indicated by visual instruction +# * clicGvideo: Left-hand three-times button press, indicated by auditory instruction +# * clicDvideo: Right-hand three-times button press, indicated by auditory instruction +# * damier_H: Visualization of flashing horizontal checkerboards +# * damier_V: Visualization of flashing vertical checkerboards +# * phraseaudio: Listen to narrative sentences +# * phrasevideo: Read narrative sentences +# * calculaudio: Mental subtraction, indicated by auditory instruction +# * calculvideo: Mental subtraction, indicated by visual instruction # -# We just get the provided file and make it BIDS-compliant. + t_r = 2.4 events_file = data['events'] import pandas as pd events= pd.read_table(events_file) -############################################################################### -# Add a column for 'duration' (filled with ones) for BIDS compliance -import numpy as np -events['duration'] = np.ones_like(events.onset) - ############################################################################### # Running a basic model # --------------------- @@ -71,6 +94,7 @@ # For this, let's create a function that, given the design matrix, # generates the corresponding contrasts. This will be useful to # repeat contrast specification when we change the design matrix. +import numpy as np def make_localizer_contrasts(design_matrix): """ returns a dictionary of four contrasts, given the design matrix""" @@ -192,7 +216,8 @@ def plot_contrast(first_level_model): # Another alternative to get a drift model is to specify a set of polynomials # Let's take a basis of 5 polynomials -first_level_model = FirstLevelModel(t_r, drift_model='polynomial', drift_order=5) +first_level_model = FirstLevelModel(t_r, drift_model='polynomial', + drift_order=5) first_level_model = first_level_model.fit(fmri_img, events=events) design_matrix = first_level_model.design_matrices_[0] plot_design_matrix(design_matrix) @@ -206,9 +231,12 @@ def plot_contrast(first_level_model): # Changing the hemodynamic response model # --------------------------------------- # -# This is the filter used to convert the event sequence into a reference BOLD signal for the design matrix. +# This is the filter used to convert the event sequence into a +# reference BOLD signal for the design matrix. # -# The first thing that we can do is to change the default model (the so-called Glover hrf) for the so-called canonical model of SPM --which has slightly weaker undershoot component. +# The first thing that we can do is to change the default model (the +# so-called Glover hrf) for the so-called canonical model of SPM +# --which has slightly weaker undershoot component. first_level_model = FirstLevelModel(t_r, hrf_model='spm') first_level_model = first_level_model.fit(fmri_img, events=events) @@ -220,7 +248,14 @@ def plot_contrast(first_level_model): ######################################################################### # No strong --positive or negative-- effect. # -# We could try to go one step further: using not only the so-called canonical hrf, but also its time derivative. Note that in that case, we still perform the contrasts and obtain statistical significance for the main effect ---not the time derivative. This means that the inclusion of time derivative in the design matrix has the sole effect of discounting timing misspecification from the error term, which vould decrease the estimated variance and enhance the statistical significance of the effect. Is it the case ? +# We could try to go one step further: using not only the so-called +# canonical hrf, but also its time derivative. Note that in that case, +# we still perform the contrasts and obtain statistical significance +# for the main effect ---not the time derivative. This means that the +# inclusion of time derivative in the design matrix has the sole +# effect of discounting timing misspecification from the error term, +# which vould decrease the estimated variance and enhance the +# statistical significance of the effect. Is it the case ? first_level_model = FirstLevelModel(t_r, hrf_model='spm + derivative') first_level_model = first_level_model.fit(fmri_img, events=events) @@ -232,8 +267,10 @@ def plot_contrast(first_level_model): ######################################################################### # Not a huge effect, but rather positive overall. We could keep that one. # -# Bzw, a benefit of this approach is that we can test which voxels are well explined by the derivative term, hinting at misfit regions, a possibly valuable information -# This is implemented by an F test on the time derivative regressors. +# Bzw, a benefit of this approach is that we can test which voxels are +# well explined by the derivative term, hinting at misfit regions, a +# possibly valuable information This is implemented by an F test on +# the time derivative regressors. contrast_val = np.eye(design_matrix.shape[1])[1:2:21] z_map = first_level_model.compute_contrast( @@ -243,11 +280,14 @@ def plot_contrast(first_level_model): plt.show() ######################################################################### -# We don't see too much here: the onset times and hrf delay we're using are probably fine. +# We don't see too much here: the onset times and hrf delay we're +# using are probably fine. ######################################################################### -# We can also consider adding the so-called dispersion derivative to capture some mis-specification in the shape of the hrf. -# this is done by specifying `hrf_model='spm + derivative + dispersion'` +# We can also consider adding the so-called dispersion derivative to +# capture some mis-specification in the shape of the hrf. +# +# This is done by specifying `hrf_model='spm + derivative + dispersion'` # first_level_model = FirstLevelModel(t_r, hrf_model='spm + derivative + dispersion') first_level_model = first_level_model.fit(fmri_img, events=events) @@ -257,13 +297,17 @@ def plot_contrast(first_level_model): plt.show() ######################################################################### -# Not a huge effect. For the sake of simplicity and readibility, we can drop that one. +# Not a huge effect. For the sake of simplicity and readibility, we +# can drop that one. ######################################################################### # The noise model ar(1) or ols ? # ------------------------------ # -# So far,we have implicitly used a lag-1 autoregressive model ---aka ar(1)--- for the temporal structure of the noise. An alternative choice is to use an ordinaly least squares model (ols) that assumes no temporal structure (time-independent noise) +# So far,we have implicitly used a lag-1 autoregressive model ---aka +# ar(1)--- for the temporal structure of the noise. An alternative +# choice is to use an ordinaly least squares model (ols) that assumes +# no temporal structure (time-independent noise) first_level_model = FirstLevelModel(t_r, hrf_model='spm + derivative', noise_model='ols') first_level_model = first_level_model.fit(fmri_img, events=events) @@ -279,11 +323,18 @@ def plot_contrast(first_level_model): # Removing confounds # ------------------ # -# A problematic feature of fMRI is the presence of unconctrolled confounds in the data, sue to scanner instabilities (spikes) or physiological phenomena, such as motion, heart and respiration-related blood oxygenation flucturations. -# Side measurements are sometimes acquired to charcterise these effects. Here we don't have access to those. -# What we can do instead is to estimate confounding effects from the data themselves, using the compcorr approach, and take those into account in the model. +# A problematic feature of fMRI is the presence of unconctrolled +# confounds in the data, sue to scanner instabilities (spikes) or +# physiological phenomena, such as motion, heart and +# respiration-related blood oxygenation flucturations. Side +# measurements are sometimes acquired to charcterise these +# effects. Here we don't have access to those. What we can do instead +# is to estimate confounding effects from the data themselves, using +# the compcorr approach, and take those into account in the model. # -# For this we rely on the so-called :ref:`high_variance_confounds ` routine of Nilearn. +# For this we rely on the so-called :ref:`high_variance_confounds +# ` +# routine of Nilearn. from nilearn.image import high_variance_confounds @@ -299,16 +350,22 @@ def plot_contrast(first_level_model): ######################################################################### # Note the five additional columns in the design matrix # -# The effect on activation maps is complex: auditory/visual effects are killed, probably because they were somewhat colinear to the confounds. On the other hand, some of the maps become cleaner (H-V, computation) after this addition. +# The effect on activation maps is complex: auditory/visual effects +# are killed, probably because they were somewhat colinear to the +# confounds. On the other hand, some of the maps become cleaner (H-V, +# computation) after this addition. ######################################################################### # Smoothing # ---------- # -# Smoothing is a regularization of the model. It has two benefits: decrease the noise level in images, and reduce the discrepancy between individuals. The drawback is that it biases the shape and position of activation. -# We simply illustrate here the statistical gains. -# We use a mild smoothing of 5mm full-width at half maximum (fwhm). +# Smoothing is a regularization of the model. It has two benefits: +# decrease the noise level in images, and reduce the discrepancy +# between individuals. The drawback is that it biases the shape and +# position of activation. We simply illustrate here the statistical +# gains. We use a mild smoothing of 5mm full-width at half maximum +# (fwhm). first_level_model = FirstLevelModel( t_r, hrf_model='spm + derivative', smoothing_fwhm=5).fit( @@ -326,9 +383,15 @@ def plot_contrast(first_level_model): # Masking # -------- # Masking consists in selecting the region of the image on which the model is run: it is useless to run it outside of the brain. -# the approach taken by FirstLeveModel is to estimate it from the fMRI data themselves when no mask is explicitly provided. -# Since the data have been resampled into MNI space, we can use instead a mask of the grey matter in MNI space. The benefit is that it makes voxel-level comparisons easier across subjects and datasets, and removed non-grey matter regions, in which no BOLD signal is expected. -# The downside is that the mask may not fit very well these particular data. +# +# The approach taken by FirstLeveModel is to estimate it from the fMRI +# data themselves when no mask is explicitly provided. Since the data +# have been resampled into MNI space, we can use instead a mask of the +# grey matter in MNI space. The benefit is that it makes voxel-level +# comparisons easier across subjects and datasets, and removed +# non-grey matter regions, in which no BOLD signal is expected. The +# downside is that the mask may not fit very well these particular +# data. from nilearn.plotting import plot_roi from nilearn.datasets import fetch_icbm152_brain_gm_mask @@ -359,5 +422,8 @@ def plot_contrast(first_level_model): # Conclusion # ---------- # -# Interestingly, the model used here seems quite resilient to manipulation of modeling parameters: this is reassuring. It shows that Nistats defaults ('cosine' drift, cutoff=128s, 'glover' hrf, ar(1) model) are actually reasonable. -# Note that these conclusions are specific to this dataset and may vary with other ones. +# Interestingly, the model used here seems quite resilient to +# manipulation of modeling parameters: this is reassuring. It shows +# that Nistats defaults ('cosine' drift, cutoff=128s, 'glover' hrf, +# ar(1) model) are actually reasonable. Note that these conclusions +# are specific to this dataset and may vary with other ones. From 7e175bfbe1fed5d4237052121b6122552b6c2fe9 Mon Sep 17 00:00:00 2001 From: bthirion Date: Mon, 8 Oct 2018 19:55:29 +0200 Subject: [PATCH 168/210] Some explaantions about localizer protocol --- examples/01_tutorials/plot_first_level_model_details.py | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/01_tutorials/plot_first_level_model_details.py b/examples/01_tutorials/plot_first_level_model_details.py index 59478482..d7454b6a 100644 --- a/examples/01_tutorials/plot_first_level_model_details.py +++ b/examples/01_tutorials/plot_first_level_model_details.py @@ -417,7 +417,6 @@ def plot_contrast(first_level_model): plt.show() - ######################################################################### # Conclusion # ---------- From 82070ef136682d10efccfcf47f9699845fc01605 Mon Sep 17 00:00:00 2001 From: bthirion Date: Mon, 8 Oct 2018 21:55:58 +0200 Subject: [PATCH 169/210] better explanations --- examples/01_tutorials/plot_first_level_model_details.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/examples/01_tutorials/plot_first_level_model_details.py b/examples/01_tutorials/plot_first_level_model_details.py index d7454b6a..2f9ee8a6 100644 --- a/examples/01_tutorials/plot_first_level_model_details.py +++ b/examples/01_tutorials/plot_first_level_model_details.py @@ -135,7 +135,12 @@ def make_localizer_contrasts(design_matrix): ######################################################################### # So let's look at these computed contrasts - +# +# * "left - right button press" probes motor activity in left versus right button presses +# * 'H-V': probes the differential activity in viewing a horizontal vs vertical checkerboard +# * "audio - video" probes the difference of activity between listening to some content or reading the same type of content (instructions, stories) +# * "computation - sentences" looks at the activity when performing a mental comptation task versus simply reading sentences. +# contrasts = make_localizer_contrasts(design_matrix) plt.figure(figsize=(5, 9)) from nistats.reporting import plot_contrast_matrix From d98379e81db34c94d37e0a644264c076f55a2d4b Mon Sep 17 00:00:00 2001 From: bthirion Date: Mon, 8 Oct 2018 22:28:48 +0200 Subject: [PATCH 170/210] A bit of cleaning on BIDS example --- examples/01_tutorials/plot_bids_analysis.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/examples/01_tutorials/plot_bids_analysis.py b/examples/01_tutorials/plot_bids_analysis.py index 80c86a97..1bc01201 100644 --- a/examples/01_tutorials/plot_bids_analysis.py +++ b/examples/01_tutorials/plot_bids_analysis.py @@ -15,8 +15,6 @@ in this case the preprocessed bold images were already normalized to the same MNI space. - - To run this example, you must launch IPython via ``ipython --matplotlib`` in a terminal, or use the Jupyter notebook. @@ -37,6 +35,10 @@ from nistats.datasets import fetch_bids_langloc_dataset data_dir, _ = fetch_bids_langloc_dataset() +############################################################################## +# Here is the location of the dataset on disk +print(data_dir) + ############################################################################## # Obtain automatically FirstLevelModel objects and fit arguments # -------------------------------------------------------------- @@ -118,6 +120,9 @@ # column names) from nistats.second_level_model import SecondLevelModel second_level_input = models + +######################################################################### +# Note that we apply a smoothing of 8mm. second_level_model = SecondLevelModel(smoothing_fwhm=8.0) second_level_model = second_level_model.fit(second_level_input) @@ -126,7 +131,8 @@ # Since we are not providing confounders we are performing an one-sample test # at the second level with the images determined by the specified first level # contrast. -zmap = second_level_model.compute_contrast(first_level_contrast='language-string') +zmap = second_level_model.compute_contrast( + first_level_contrast='language-string') ######################################################################### # The group level contrast reveals a left lateralized fronto-temporal From 4be29274fae131b36052c7f1c087b10f7deec519 Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Tue, 9 Oct 2018 17:54:41 +0200 Subject: [PATCH 171/210] Updated Whats New and version number --- doc/whats_new.rst | 58 +++++++++++++++++++++++++++++++++++----------- nistats/version.py | 2 +- 2 files changed, 45 insertions(+), 15 deletions(-) diff --git a/doc/whats_new.rst b/doc/whats_new.rst index ff887381..5849e2f2 100644 --- a/doc/whats_new.rst +++ b/doc/whats_new.rst @@ -4,26 +4,56 @@ Changelog --------- -Updated the minimum versions of the dependencies +* Updated the minimum versions of the dependencies + * Numpy >= 1.11 + * SciPy >= 0.17 + * Nibabel >= 2.0.2 + * Nilearn >= 0.4.0 + * Pandas >= 0.18.0 + * Sklearn >= 0.18.0 -* Numpy >= 1.11 -* SciPy >= 0.17 -* Nibabel >= 2.0.2 -* Nilearn >= 0.4.0 -* Pandas >= 0.18.0 -* Sklearn >= 0.18.0 +* Added comprehensive tutorial -* third argument of map_threshold is now called 'level'. +* Second-level model accepts 4D images as input. -* Added comprehensive tutorial +* Changes in function parameters + * third argument of map_threshold is now called ``level``. + * Changed the defaut oversampling value for the hemodynamic response + to 50 and exposed this parameter. + * changed the term ``paradigm`` to ``events`` and made it + BIDS-compliant. Set the event file to be tab-separated + +* Certain functions and methods have been renamed for clarity + * ``nistats.design_matrix`` + * ``make_design_matrix() -> make_first_level_design_matrix()`` + * ``create_second_level_design() -> make_second_level_design_matrix()`` + * ``nistats.utils`` + * ``pos_recipr() -> positive_reciprocal()`` + * ``multiple_fast_inv() -> multiple_fast_inverse()`` + +* Python2 Deprecation: + Python 2 is now deprecated and will not be supported in a future version. + A DeprecationWarning is displayed in Python 2 environments with a suggestion to move to Python 3. + + +Contributors +------------ + +The following people contributed to this release:: -* changed the term `paradigm` to `events` and made it - BIDS-compliant. Set the event file to be tab-separated + 45 Bertrand Thirion + 70 Kshitij Chawla + 16 Taylor Salo + 6 KamalakerDadi + 5 chrplr + 5 hcherkaoui + 5 rschmaelzle + 4 mannalytics + 3 Martin Perez-Guevara + 2 Christopher J. Markiewicz + 1 Loïc Estève -* Changed the defaut oversampling value for the hemodynamic response - to 50 and exposed this parameter. -* Second-level model accepts 4D images as input. 0.0.1a ======= diff --git a/nistats/version.py b/nistats/version.py index 9a094a4d..5b8d3c44 100644 --- a/nistats/version.py +++ b/nistats/version.py @@ -21,7 +21,7 @@ # Dev branch marker is: 'X.Y.dev' or 'X.Y.devN' where N is an integer. # 'X.Y.dev0' is the canonical version of 'X.Y.dev' # -__version__ = '0.0.1a' +__version__ = '0.0.1b' _NISTATS_INSTALL_MSG = 'See %s for installation information.' % ( 'http://nistats.github.io/introduction.html#installation') From 96cc8d5a6ccca29cfdd652616a8f9fcfc4ca59cd Mon Sep 17 00:00:00 2001 From: bthirion Date: Thu, 11 Oct 2018 23:17:01 +0200 Subject: [PATCH 172/210] Added integrated example of surface-based analysis --- .../plot_surface_bids_analysis.py | 162 ++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 examples/01_tutorials/plot_surface_bids_analysis.py diff --git a/examples/01_tutorials/plot_surface_bids_analysis.py b/examples/01_tutorials/plot_surface_bids_analysis.py new file mode 100644 index 00000000..2671f057 --- /dev/null +++ b/examples/01_tutorials/plot_surface_bids_analysis.py @@ -0,0 +1,162 @@ +"""Surface-based dataset first and second level analysis of a dataset +================================================================== + + +Full step-by-step example of fitting a GLM (first and second level +analysis) in a 10-subjects dataset and visualizing the results. + +More specifically: + +1. Download an fMRI BIDS dataset with two language conditions to contrast. +2. Project the data to a standard mesh, fsaverage5, aka the Freesurfer +template mesh downsampled to about 10k nodes per hemisphere. +3. Run the first level model objects +4. Fit a second level model on the fitted first level models. + +Notice that in this case the preprocessed bold images were already + normalized to the same MNI space. + +To run this example, you must launch IPython via ``ipython +--matplotlib`` in a terminal, or use the Jupyter notebook. + +.. contents:: **Contents** + :local: + :depth: 1 + +""" + +############################################################################## +# Fetch example BIDS dataset +# -------------------------- +# We download an simplified BIDS dataset made available for illustrative +# purposes. It contains only the necessary +# information to run a statistical analysis using Nistats. The raw data +# subject folders only contain bold.json and events.tsv files, while the +# derivatives folder with preprocessed files contain preproc.nii and +# confounds.tsv files. +from nistats.datasets import fetch_bids_langloc_dataset +data_dir, _ = fetch_bids_langloc_dataset() + +############################################################################## +# Here is the location of the dataset on disk +print(data_dir) + +############################################################################## +# Obtain automatically FirstLevelModel objects and fit arguments +# -------------------------------------------------------------- +# From the dataset directory we obtain automatically FirstLevelModel objects +# with their subject_id filled from the BIDS dataset. Moreover we obtain +# for each model a dictionary with run_imgs, events and confounder regressors +# since in this case a confounds.tsv file is available in the BIDS dataset. +# To get the first level models we only have to specify the dataset directory +# and the task_label as specified in the file names. +from nistats.first_level_model import first_level_models_from_bids +task_label = 'languagelocalizer' +space_label = 'MNI152nonlin2009aAsym' +_, models_run_imgs, models_events, models_confounds = \ + first_level_models_from_bids( + data_dir, task_label, space_label, + img_filters=[('variant', 'smoothResamp')]) + +############################################################################# +# We also need to get the TR information. For that we use a json file +# of the dataset +import os +json_file = os.path.join(data_dir, 'sub-01', 'ses-02', 'func', + 'sub-01_ses-02_task-languagelocalizer_bold.json') +import json +with open(json_file, 'r') as f: + t_r = json.load(f)['RepetitionTime'] + +############################################################################# +# Project fMRI data to the surface: First get fsaverage5 +from nilearn.datasets import fetch_surf_fsaverage +fsaverage = fetch_surf_fsaverage(mesh='fsaverage5') + +######################################################################### +# The projection function simply takes the fMRI data and the mesh. +# Note that those correspond spatially, are they are bothin MNI space. +import numpy as np +from nilearn import surface +from nistats.design_matrix import make_first_level_design_matrix +from nistats.first_level_model import run_glm +from nistats.contrasts import compute_contrast + +######################################################################### +# Empty lists in which we are going to store activation values. +z_scores_right = [] +z_scores_left = [] +for (fmri_img, confound, events) in zip( + models_run_imgs, models_confounds, models_events): + texture = surface.vol_to_surf(fmri_img[0], fsaverage.pial_right) + n_scans = texture.shape[1] + frame_times = t_r * (np.arange(n_scans) + .5) + + # Create the design matrix + # + # We specify an hrf model containing Glover model and its time derivative + # the drift model is implicitly a cosine basis with period cutoff 128s. + design_matrix = make_first_level_design_matrix( + frame_times, events=events[0], hrf_model='glover + derivative', + add_regs=confound[0]) + + # contrast_specification + contrast_values = (design_matrix.columns == 'language') * 1.0 -\ + (design_matrix.columns == 'string') + + # Setup and fit GLM. + # Note that the output consists in 2 variables: `labels` and `fit` + # `labels` tags voxels according to noise autocorrelation. + # `estimates` contains the parameter estimates. + # We input them for contrast computation. + labels, estimates = run_glm(texture.T, design_matrix.values) + contrast = compute_contrast(labels, estimates, contrast_values, + contrast_type='t') + # we present the Z-transform of the t map + z_score = contrast.z_score() + z_scores_right.append(z_score) + + # Do the left hemipshere exactly in the same way + texture = surface.vol_to_surf(fmri_img, fsaverage.pial_left) + labels, estimates = run_glm(texture.T, design_matrix.values) + contrast = compute_contrast(labels, estimates, contrast_values, + contrast_type='t') + z_scores_left.append(contrast.z_score()) + +############################################################################ +# Individual activation maps have been accumulated in the z_score_left +# and az_scores_right lists respectively. We can now use them in a +# group study (one -sample study) + +############################################################################ +# Group study +# ----------- +# +# Prepare figure for concurrent plot of individual maps +# compute population-level maps for left and right hemisphere +# we directetly do that on the values arrays +from scipy.stats import ttest_1samp, norm +t_left, pval_left = ttest_1samp(np.array(z_scores_left), 0) +t_right, pval_right = ttest_1samp(np.array(z_scores_right), 0) + +############################################################################ +# What we have so far are p-values: we convert them to z-values for plotting +z_val_left = norm.isf(pval_left) +z_val_right = norm.isf(pval_right) + +############################################################################ +# Plot the resulting maps. +# Left hemipshere +from nilearn import plotting +plotting.plot_surf_stat_map( + fsaverage.infl_left, z_val_left, hemi='left', + title="language-string, left hemisphere", colorbar=True, + threshold=3., bg_map=fsaverage.sulc_left) +############################################################################ +# Right hemisphere +plotting.plot_surf_stat_map( + fsaverage.infl_right, z_val_left, hemi='right', + title="language-string, right hemisphere", colorbar=True, + threshold=3., bg_map=fsaverage.sulc_right) + +plotting.show() From acbbfe94434f08c6ddaaaf6be342fbf554606909 Mon Sep 17 00:00:00 2001 From: bthirion Date: Thu, 11 Oct 2018 23:19:38 +0200 Subject: [PATCH 173/210] Reorganizing the examples --- .../{01_tutorials => 05_complete_examples}/plot_bids_analysis.py | 0 .../plot_surface_bids_analysis.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename examples/{01_tutorials => 05_complete_examples}/plot_bids_analysis.py (100%) rename examples/{01_tutorials => 05_complete_examples}/plot_surface_bids_analysis.py (100%) diff --git a/examples/01_tutorials/plot_bids_analysis.py b/examples/05_complete_examples/plot_bids_analysis.py similarity index 100% rename from examples/01_tutorials/plot_bids_analysis.py rename to examples/05_complete_examples/plot_bids_analysis.py diff --git a/examples/01_tutorials/plot_surface_bids_analysis.py b/examples/05_complete_examples/plot_surface_bids_analysis.py similarity index 100% rename from examples/01_tutorials/plot_surface_bids_analysis.py rename to examples/05_complete_examples/plot_surface_bids_analysis.py From 99d3d4ce3256dfd068ef4e959efb3cf1a1bee8f3 Mon Sep 17 00:00:00 2001 From: Gael Varoquaux Date: Fri, 12 Oct 2018 10:00:01 +0200 Subject: [PATCH 174/210] Better intall instructions in README.rst. This was confusing users --- README.rst | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 4744e6b0..cfc0f374 100644 --- a/README.rst +++ b/README.rst @@ -49,10 +49,8 @@ If you want to download openneuro datasets Boto3 >= 1.2 is required Install ======= -In order to perform the installation, run the following command from the nistats directory:: - - python setup.py install --user - +The installation instructions are found on the webpage: +https://nistats.github.io/ Development =========== From fafc35f1f60e1cfc9fd4daa38a92cae94bfea866 Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Fri, 12 Oct 2018 15:46:46 +0200 Subject: [PATCH 175/210] Added 0.0.1b release in News section --- doc/themes/nistats/layout.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/themes/nistats/layout.html b/doc/themes/nistats/layout.html index cb4748c1..cfacc5a8 100644 --- a/doc/themes/nistats/layout.html +++ b/doc/themes/nistats/layout.html @@ -191,6 +191,8 @@

    Functional MRI Neuro-Imaging in Python

    News

      +
    • October 2018: Nistats 0.0.1b released +

    • November 2017: Nistats 0.0.1a released

    From a70a08e49525c9950722f793bdc0fcdfcbfc9cdd Mon Sep 17 00:00:00 2001 From: bthirion Date: Fri, 12 Oct 2018 16:10:28 +0200 Subject: [PATCH 176/210] Cleaned derivative testing --- .../plot_first_level_model_details.py | 59 ++++++++++++++----- 1 file changed, 44 insertions(+), 15 deletions(-) diff --git a/examples/01_tutorials/plot_first_level_model_details.py b/examples/01_tutorials/plot_first_level_model_details.py index 2f9ee8a6..2cbb60cd 100644 --- a/examples/01_tutorials/plot_first_level_model_details.py +++ b/examples/01_tutorials/plot_first_level_model_details.py @@ -277,7 +277,7 @@ def plot_contrast(first_level_model): # possibly valuable information This is implemented by an F test on # the time derivative regressors. -contrast_val = np.eye(design_matrix.shape[1])[1:2:21] +contrast_val = np.eye(design_matrix.shape[1])[1:21:2] z_map = first_level_model.compute_contrast( contrast_val, output_type='z_score') plotting.plot_stat_map( @@ -285,8 +285,21 @@ def plot_contrast(first_level_model): plt.show() ######################################################################### -# We don't see too much here: the onset times and hrf delay we're -# using are probably fine. +# Well, there seems to be something here. Maybe we could adjust the +# timing, by increasing the slice_time_ref parameter: 0 to 0.5 now the +# reference for model sampling is not the beginning of the volume +# acquisition, but the middle of it. +first_level_model = FirstLevelModel(t_r, hrf_model='spm + derivative', + slice_time_ref=0.5) +first_level_model = first_level_model.fit(fmri_img, events=events) +z_map = first_level_model.compute_contrast( + contrast_val, output_type='z_score') +plotting.plot_stat_map( + z_map, display_mode='z', threshold=3.0, + title='effect of time derivatives after model shift') +plt.show() +######################################################################### +# The time derivatives regressors capture less signal: it's better so. ######################################################################### # We can also consider adding the so-called dispersion derivative to @@ -294,7 +307,8 @@ def plot_contrast(first_level_model): # # This is done by specifying `hrf_model='spm + derivative + dispersion'` # -first_level_model = FirstLevelModel(t_r, hrf_model='spm + derivative + dispersion') +first_level_model = FirstLevelModel(t_r,slice_time_ref=0.5, + hrf_model='spm + derivative + dispersion') first_level_model = first_level_model.fit(fmri_img, events=events) design_matrix = first_level_model.design_matrices_[0] plot_design_matrix(design_matrix) @@ -314,7 +328,9 @@ def plot_contrast(first_level_model): # choice is to use an ordinaly least squares model (ols) that assumes # no temporal structure (time-independent noise) -first_level_model = FirstLevelModel(t_r, hrf_model='spm + derivative', noise_model='ols') +first_level_model = FirstLevelModel(t_r, slice_time_ref=0.5, + hrf_model='spm + derivative', + noise_model='ols') first_level_model = first_level_model.fit(fmri_img, events=events) design_matrix = first_level_model.design_matrices_[0] plot_design_matrix(design_matrix) @@ -322,7 +338,8 @@ def plot_contrast(first_level_model): plt.show() ######################################################################### -# While the difference is not obvious you should rather stick to the ar(1) model, which is arguably more accurate. +# While the difference is not obvious you should rather stick to the +# ar(1) model, which is arguably more accurate. ######################################################################### # Removing confounds @@ -344,7 +361,8 @@ def plot_contrast(first_level_model): from nilearn.image import high_variance_confounds confounds = pd.DataFrame(high_variance_confounds(fmri_img, percentile=1)) -first_level_model = FirstLevelModel(t_r, hrf_model='spm + derivative') +first_level_model = FirstLevelModel(t_r, hrf_model='spm + derivative', + slice_time_ref=0.5) first_level_model = first_level_model.fit(fmri_img, events=events, confounds=confounds) design_matrix = first_level_model.design_matrices_[0] @@ -373,8 +391,8 @@ def plot_contrast(first_level_model): # (fwhm). first_level_model = FirstLevelModel( - t_r, hrf_model='spm + derivative', smoothing_fwhm=5).fit( - fmri_img, events=events, confounds=confounds) + t_r, hrf_model='spm + derivative', smoothing_fwhm=5, + slice_time_ref=0.5).fit(fmri_img, events=events, confounds=confounds) design_matrix = first_level_model.design_matrices_[0] plot_design_matrix(design_matrix) plot_contrast(first_level_model) @@ -387,7 +405,9 @@ def plot_contrast(first_level_model): ######################################################################### # Masking # -------- -# Masking consists in selecting the region of the image on which the model is run: it is useless to run it outside of the brain. +# +# Masking consists in selecting the region of the image on which the +# model is run: it is useless to run it outside of the brain. # # The approach taken by FirstLeveModel is to estimate it from the fMRI # data themselves when no mask is explicitly provided. Since the data @@ -398,10 +418,11 @@ def plot_contrast(first_level_model): # downside is that the mask may not fit very well these particular # data. -from nilearn.plotting import plot_roi +data_mask = first_level_model.masker_.mask_img_ from nilearn.datasets import fetch_icbm152_brain_gm_mask icbm_mask = fetch_icbm152_brain_gm_mask() -data_mask = first_level_model.masker_.mask_img_ + +from nilearn.plotting import plot_roi plt.figure(figsize=(16, 4)) ax = plt.subplot(121) plot_roi(icbm_mask, title='ICBM mask', axes=ax) @@ -410,17 +431,25 @@ def plot_contrast(first_level_model): plt.show() ######################################################################### -# Impact on the first-level model - +# For the sake of time saving, we reample icbm_mask to our data +# We use interpolation = 'nearest' to keep the mask a binary image +from nilearn.image import resample_to_img +resampled_icbm_mask = resample_to_img(icbm_mask, data_mask, + interpolation='nearest') +######################################################################### +# Impact on the first-level model first_level_model = FirstLevelModel( - t_r, hrf_model='spm + derivative', smoothing_fwhm=5).fit( + t_r, hrf_model='spm + derivative', smoothing_fwhm=5, slice_time_ref=0.5, + mask=resampled_icbm_mask).fit( fmri_img, events=events, confounds=confounds) design_matrix = first_level_model.design_matrices_[0] plot_design_matrix(design_matrix) plot_contrast(first_level_model) plt.show() +######################################################################### +# Note that it removed suprious spots in the white matter. ######################################################################### # Conclusion From f38d8abc0381574a1c5de6428a0ade4e167fe1a8 Mon Sep 17 00:00:00 2001 From: bthirion Date: Fri, 12 Oct 2018 17:10:26 +0200 Subject: [PATCH 177/210] fixed contrast plotting --- examples/01_tutorials/plot_first_level_model_details.py | 6 +++++- nistats/reporting.py | 5 ++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/examples/01_tutorials/plot_first_level_model_details.py b/examples/01_tutorials/plot_first_level_model_details.py index 2cbb60cd..53af77b7 100644 --- a/examples/01_tutorials/plot_first_level_model_details.py +++ b/examples/01_tutorials/plot_first_level_model_details.py @@ -145,7 +145,7 @@ def make_localizer_contrasts(design_matrix): plt.figure(figsize=(5, 9)) from nistats.reporting import plot_contrast_matrix for i, (key, values) in enumerate(contrasts.items()): - ax = plt.subplot(5, 1, i + 1) + ax = plt.subplot(len(contrasts) + 1, 1, i + 1) plot_contrast_matrix(values, design_matrix=design_matrix, ax=ax) plt.show() @@ -278,6 +278,9 @@ def plot_contrast(first_level_model): # the time derivative regressors. contrast_val = np.eye(design_matrix.shape[1])[1:21:2] +plot_contrast_matrix(contrast_val, design_matrix) +plt.show() + z_map = first_level_model.compute_contrast( contrast_val, output_type='z_score') plotting.plot_stat_map( @@ -432,6 +435,7 @@ def plot_contrast(first_level_model): ######################################################################### # For the sake of time saving, we reample icbm_mask to our data +# For this we call the resample_to_img routine of Nilearn. # We use interpolation = 'nearest' to keep the mask a binary image from nilearn.image import resample_to_img resampled_icbm_mask = resample_to_img(icbm_mask, data_mask, diff --git a/nistats/reporting.py b/nistats/reporting.py index 15bd4334..10fc4813 100644 --- a/nistats/reporting.py +++ b/nistats/reporting.py @@ -378,9 +378,8 @@ def plot_contrast_matrix(contrast_def, design_matrix, colorbar=False, ax=None, ax.set_yticklabels(['' for x in ax.get_yticklabels()]) # Shift ticks to be at 0.5, 1.5, etc - ax.xaxis.set(ticks=np.arange(1.0, len(design_column_names) + 1.0), - ticklabels=design_column_names) - ax.set_xticklabels(design_column_names, rotation=60, ha='right') + ax.xaxis.set(ticks=np.arange(len(design_column_names))) + ax.set_xticklabels(design_column_names, rotation=60, ha='left') if colorbar: plt.colorbar(mat, fraction=0.025, pad=0.04) From 0f12cc07e0ae8a2052ff0f48651df7907b588f48 Mon Sep 17 00:00:00 2001 From: bthirion Date: Mon, 15 Oct 2018 14:58:57 +0200 Subject: [PATCH 178/210] cleaned generated output of tests --- nistats/tests/test_first_level_model.py | 52 ++++++++++++------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/nistats/tests/test_first_level_model.py b/nistats/tests/test_first_level_model.py index ef834807..6b8b0ff7 100644 --- a/nistats/tests/test_first_level_model.py +++ b/nistats/tests/test_first_level_model.py @@ -76,32 +76,32 @@ def test_high_level_glm_one_session(): def test_high_level_glm_with_data(): # New API - shapes, rk = ((7, 8, 7, 15), (7, 8, 7, 16)), 3 - mask, fmri_data, design_matrices = write_fake_fmri_data(shapes, rk) - - multi_session_model = FirstLevelModel(mask=None).fit( - fmri_data, design_matrices=design_matrices) - n_voxels = multi_session_model.masker_.mask_img_.get_data().sum() - z_image = multi_session_model.compute_contrast(np.eye(rk)[1]) - assert_equal(np.sum(z_image.get_data() != 0), n_voxels) - assert_true(z_image.get_data().std() < 3.) - - # with mask - multi_session_model = FirstLevelModel(mask=mask).fit( - fmri_data, design_matrices=design_matrices) - z_image = multi_session_model.compute_contrast( - np.eye(rk)[:2], output_type='z_score') - p_value = multi_session_model.compute_contrast( - np.eye(rk)[:2], output_type='p_value') - stat_image = multi_session_model.compute_contrast( - np.eye(rk)[:2], output_type='stat') - effect_image = multi_session_model.compute_contrast( - np.eye(rk)[:2], output_type='effect_size') - variance_image = multi_session_model.compute_contrast( - np.eye(rk)[:2], output_type='effect_variance') - assert_array_equal(z_image.get_data() == 0., load(mask).get_data() == 0.) - assert_true( - (variance_image.get_data()[load(mask).get_data() > 0] > .001).all()) + with InTemporaryDirectory(): + shapes, rk = ((7, 8, 7, 15), (7, 8, 7, 16)), 3 + mask, fmri_data, design_matrices = write_fake_fmri_data(shapes, rk) + multi_session_model = FirstLevelModel(mask=None).fit( + fmri_data, design_matrices=design_matrices) + n_voxels = multi_session_model.masker_.mask_img_.get_data().sum() + z_image = multi_session_model.compute_contrast(np.eye(rk)[1]) + assert_equal(np.sum(z_image.get_data() != 0), n_voxels) + assert_true(z_image.get_data().std() < 3.) + + # with mask + multi_session_model = FirstLevelModel(mask=mask).fit( + fmri_data, design_matrices=design_matrices) + z_image = multi_session_model.compute_contrast( + np.eye(rk)[:2], output_type='z_score') + p_value = multi_session_model.compute_contrast( + np.eye(rk)[:2], output_type='p_value') + stat_image = multi_session_model.compute_contrast( + np.eye(rk)[:2], output_type='stat') + effect_image = multi_session_model.compute_contrast( + np.eye(rk)[:2], output_type='effect_size') + variance_image = multi_session_model.compute_contrast( + np.eye(rk)[:2], output_type='effect_variance') + assert_array_equal(z_image.get_data() == 0., load(mask).get_data() == 0.) + assert_true( + (variance_image.get_data()[load(mask).get_data() > 0] > .001).all()) def test_high_level_glm_with_paths(): From 6f4d80d52ab40be221f665dd620e2c1e22d31808 Mon Sep 17 00:00:00 2001 From: btnguyen Date: Tue, 16 Oct 2018 17:08:21 +0200 Subject: [PATCH 179/210] remove unused import --- nistats/first_level_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nistats/first_level_model.py b/nistats/first_level_model.py index 767bede8..27136443 100644 --- a/nistats/first_level_model.py +++ b/nistats/first_level_model.py @@ -20,7 +20,7 @@ import numpy as np import pandas as pd -from nibabel import Nifti1Image, AnalyzeImage +from nibabel import Nifti1Image from sklearn.base import BaseEstimator, TransformerMixin, clone from sklearn.externals.joblib import Memory From 423dcf2fdffabb2cf4d95cdff83ef8bbfab09e1b Mon Sep 17 00:00:00 2001 From: btnguyen Date: Tue, 16 Oct 2018 18:01:29 +0200 Subject: [PATCH 180/210] fix memory caching not working properly when doing compute_contrast --- nistats/second_level_model.py | 11 ++++--- nistats/tests/test_second_level_model.py | 41 ++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/nistats/second_level_model.py b/nistats/second_level_model.py index aa1996f0..fbd8b0d2 100644 --- a/nistats/second_level_model.py +++ b/nistats/second_level_model.py @@ -16,6 +16,7 @@ from nibabel import Nifti1Image from sklearn.base import BaseEstimator, TransformerMixin, clone +from sklearn.externals.joblib import Memory from nilearn._utils.niimg_conversions import check_niimg from nilearn._utils import CacheMixin from nilearn.input_data import NiftiMasker @@ -101,11 +102,14 @@ class SecondLevelModel(BaseEstimator, TransformerMixin, CacheMixin): """ def __init__(self, mask=None, smoothing_fwhm=None, - memory=None, memory_level=1, verbose=0, + memory=Memory(None), memory_level=1, verbose=0, n_jobs=1, minimize_memory=True): self.mask = mask self.smoothing_fwhm = smoothing_fwhm - self.memory = memory + if isinstance(memory, _basestring): + self.memory = Memory(memory) + else: + self.memory = memory self.memory_level = memory_level self.verbose = verbose self.n_jobs = n_jobs @@ -406,8 +410,7 @@ def compute_contrast( # Fit an OLS regression for parametric statistics Y = self.masker_.transform(effect_maps) if self.memory is not None: - arg_ignore = ['n_jobs'] - mem_glm = self.memory.cache(run_glm, ignore=arg_ignore) + mem_glm = self.memory.cache(run_glm, ignore=['n_jobs']) else: mem_glm = run_glm labels, results = mem_glm(Y, self.design_matrix_.values, diff --git a/nistats/tests/test_second_level_model.py b/nistats/tests/test_second_level_model.py index 48841404..e41a472f 100644 --- a/nistats/tests/test_second_level_model.py +++ b/nistats/tests/test_second_level_model.py @@ -205,3 +205,44 @@ def test_second_level_model_contrast_computation(): X = pd.DataFrame(np.random.rand(4, 2), columns=['r1', 'r2']) model = model.fit(Y, design_matrix=X) assert_raises(ValueError, model.compute_contrast, None) + + +def test_second_level_model_contrast_computation_with_mem(): + with InTemporaryDirectory(): + shapes = ((7, 8, 9, 1),) + mask, FUNCFILE, _ = write_fake_fmri_data(shapes) + FUNCFILE = FUNCFILE[0] + func_img = load(FUNCFILE) + # ols case + model = SecondLevelModel(mask=mask, memory='nilearn_cache') + # asking for contrast before model fit gives error + assert_raises(ValueError, model.compute_contrast, 'intercept') + # fit model + Y = [func_img] * 4 + X = pd.DataFrame([[1]] * 4, columns=['intercept']) + model = model.fit(Y, design_matrix=X) + ncol = len(model.design_matrix_.columns) + c1, cnull = np.eye(ncol)[0, :], np.zeros(ncol) + # smoke test for different contrasts in fixed effects + model.compute_contrast(c1) + model.compute_contrast(c1, output_type='z_score') + model.compute_contrast(c1, output_type='stat') + model.compute_contrast(c1, output_type='p_value') + model.compute_contrast(c1, output_type='effect_size') + model.compute_contrast(c1, output_type='effect_variance') + # formula should work (passing variable name directly) + model.compute_contrast('intercept') + # or simply pass nothing + model.compute_contrast() + # passing null contrast should give back a value error + assert_raises(ValueError, model.compute_contrast, cnull) + # passing wrong parameters + assert_raises(ValueError, model.compute_contrast, []) + assert_raises(ValueError, model.compute_contrast, c1, None, '') + assert_raises(ValueError, model.compute_contrast, c1, None, []) + assert_raises(ValueError, model.compute_contrast, c1, None, None, '') + # check that passing no explicit contrast when the dsign + # matrix has morr than one columns raises an error + X = pd.DataFrame(np.random.rand(4, 2), columns=['r1', 'r2']) + model = model.fit(Y, design_matrix=X) + assert_raises(ValueError, model.compute_contrast, None) From a5365a469942d7a993e61f04463bfb20be0d9085 Mon Sep 17 00:00:00 2001 From: btnguyen Date: Wed, 17 Oct 2018 11:46:35 +0200 Subject: [PATCH 181/210] simplify test case for memory caching --- nistats/tests/test_second_level_model.py | 25 ++---------------------- 1 file changed, 2 insertions(+), 23 deletions(-) diff --git a/nistats/tests/test_second_level_model.py b/nistats/tests/test_second_level_model.py index e41a472f..c4f5739b 100644 --- a/nistats/tests/test_second_level_model.py +++ b/nistats/tests/test_second_level_model.py @@ -215,34 +215,13 @@ def test_second_level_model_contrast_computation_with_mem(): func_img = load(FUNCFILE) # ols case model = SecondLevelModel(mask=mask, memory='nilearn_cache') - # asking for contrast before model fit gives error - assert_raises(ValueError, model.compute_contrast, 'intercept') # fit model Y = [func_img] * 4 X = pd.DataFrame([[1]] * 4, columns=['intercept']) model = model.fit(Y, design_matrix=X) ncol = len(model.design_matrix_.columns) - c1, cnull = np.eye(ncol)[0, :], np.zeros(ncol) - # smoke test for different contrasts in fixed effects - model.compute_contrast(c1) + c1 = np.eye(ncol)[0, :] + # test memory caching for compute_contrast model.compute_contrast(c1, output_type='z_score') - model.compute_contrast(c1, output_type='stat') - model.compute_contrast(c1, output_type='p_value') - model.compute_contrast(c1, output_type='effect_size') - model.compute_contrast(c1, output_type='effect_variance') - # formula should work (passing variable name directly) - model.compute_contrast('intercept') # or simply pass nothing model.compute_contrast() - # passing null contrast should give back a value error - assert_raises(ValueError, model.compute_contrast, cnull) - # passing wrong parameters - assert_raises(ValueError, model.compute_contrast, []) - assert_raises(ValueError, model.compute_contrast, c1, None, '') - assert_raises(ValueError, model.compute_contrast, c1, None, []) - assert_raises(ValueError, model.compute_contrast, c1, None, None, '') - # check that passing no explicit contrast when the dsign - # matrix has morr than one columns raises an error - X = pd.DataFrame(np.random.rand(4, 2), columns=['r1', 'r2']) - model = model.fit(Y, design_matrix=X) - assert_raises(ValueError, model.compute_contrast, None) From 4edfb1c1e73f2ce26debae208450ec7a0de0edaf Mon Sep 17 00:00:00 2001 From: btnguyen Date: Wed, 17 Oct 2018 15:04:41 +0200 Subject: [PATCH 182/210] simplify memory caching if statement --- nistats/first_level_model.py | 4 ++-- nistats/second_level_model.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/nistats/first_level_model.py b/nistats/first_level_model.py index 27136443..986547cf 100644 --- a/nistats/first_level_model.py +++ b/nistats/first_level_model.py @@ -452,11 +452,11 @@ def fit(self, run_imgs, events=None, confounds=None, if self.verbose > 1: t_masking = time.time() - t_masking - sys.stderr.write('Masker took %d seconds \n' % t_masking) + sys.stderr.write('Masker took %d seconds \n' % t_masking) if self.signal_scaling: Y, _ = mean_scaling(Y, self.scaling_axis) - if self.memory is not None: + if self.memory: mem_glm = self.memory.cache(run_glm, ignore=['n_jobs']) else: mem_glm = run_glm diff --git a/nistats/second_level_model.py b/nistats/second_level_model.py index fbd8b0d2..5c5b6958 100644 --- a/nistats/second_level_model.py +++ b/nistats/second_level_model.py @@ -409,7 +409,7 @@ def compute_contrast( # Fit an OLS regression for parametric statistics Y = self.masker_.transform(effect_maps) - if self.memory is not None: + if self.memory: mem_glm = self.memory.cache(run_glm, ignore=['n_jobs']) else: mem_glm = run_glm @@ -423,7 +423,7 @@ def compute_contrast( self.results_ = results # We compute contrast object - if self.memory is not None: + if self.memory: mem_contrast = self.memory.cache(compute_contrast) else: mem_contrast = compute_contrast From 002c38d60290a9d4d834fa1777fe36096ab62d85 Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Thu, 15 Nov 2018 15:32:15 +0100 Subject: [PATCH 183/210] Fixed: Typos in docs- User Guide: A primer on BOLD-fMRI data analysis - Some language tweaks as well. --- doc/introduction.rst | 47 ++++++++++++++++++++++---------------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/doc/introduction.rst b/doc/introduction.rst index b2d13eb2..b24ebfb3 100644 --- a/doc/introduction.rst +++ b/doc/introduction.rst @@ -12,13 +12,13 @@ What is nistats? .. topic:: **What is nistats?** - Nistats is a Python module to perform voxel-wise analyses of functional magnetic resonance images (fMRI) using linear models. It provides functions to create design matrices, at the subject and group levels, to estimate them from images series and to compute statistical maps (contrasts). It allows to perform the same statistical analyses as `a SPM`_ or `a FSL`_ (but it does not provide tools for preprocessing stages (realignment, spatial normalization, etc.); for this, see `a nipype`_. + Nistats is a Python module to perform voxel-wise analyses of functional magnetic resonance images (fMRI) using linear models. It provides functions to create design matrices, at the subject and group levels, to estimate them from images series and to compute statistical maps (contrasts). It allows to perform the same statistical analyses as `SPM`_ or `FSL`_ (but it does not provide tools for preprocessing stages (realignment, spatial normalization, etc.); for this, see `nipype`_. -.. _a SPM: https://www.fil.ion.ucl.ac.uk/spm/ +.. _SPM: https://www.fil.ion.ucl.ac.uk/spm/ -.. _a FSL: https://www.fmrib.ox.ac.uk/fsl +.. _FSL: https://www.fmrib.ox.ac.uk/fsl -.. _a nipype: https://nipype.readthedocs.io/en/latest/ +.. _nipype: https://nipype.readthedocs.io/en/latest/ @@ -28,18 +28,18 @@ A primer on BOLD-fMRI data analysis What is fMRI ? -------------- -Functional magnetic resonance imaging (fMRI) is based on the fact that when local neural activity increases, increases in metabolism and blood flow lead to fluctuations of the relative concentrations of oxyhemoglobin (the red cells in the blood that carry oxygen) and deoxyhemoglobin (the same red cells after they have delivered the oxygen). Because oxy- and deoxy-hemoglobin have different magnetic properties (one is diamagnetic while the other is paramagnetic), they affect the local magnetic field in different ways. The signal picked up by the MRI scanner is sensitive to these modifications of the local magnetic field. To record cerebral activity, during functional sessions, the scanner is tuned to detect this "Blood Oxygen Level Dependent" (BOLD) signal. +Functional magnetic resonance imaging (fMRI) is based on the fact that when local neural activity increases, increases in metabolism and blood flow lead to fluctuations of the relative concentrations of oxyhaemoglobin (the red cells in the blood that carry oxygen) and deoxyhaemoglobin (the same red cells after they have delivered the oxygen). Oxy-haemoglobin and deoxy-haemoglobin have different magnetic properties (diamagnetic and paramagnetic, respectively), and they affect the local magnetic field in different ways. The signal picked up by the MRI scanner is sensitive to these modifications of the local magnetic field. To record cerebral activity, during functional sessions, the scanner is tuned to detect this "Blood Oxygen Level Dependent" (BOLD) signal. Brain activity is measured in sessions that span several minutes, while the participant performs some a cognitive task and the scanner acquires brain images, typically every 2 or 3 seconds (the time between two successive image acquisition is called the Repetition time, or TR). A cerebral MR image provides a 3D image of the brain that can be decomposed into `voxels`_ (the equivalent of pixels, but in 3 dimensions). The series of images acquired during a functional session provides, in each voxel, a time series of positive real number representing the MRI signal, sampled at the TR. -.. _a voxels: https://en.wikipedia.org/wiki/Voxel +.. _voxels: https://en.wikipedia.org/wiki/Voxel -.. note:: Before fMRI images can be used to do meaningful comparisons, they must be processed to ensure that the voxels that are being compared represent the same brain regions, irrespective of the variability in size and shape of the brain and its microarchitecture across different subjects in the experiment. The process is called spatial registration or spatial normalization. During this procedure, the voxels of all the brain images are 'registered' to correspond to the same region of the brain. Usually, the images (their voxels) are registered to a standard 'template' brain image (its voxels) . One often used standard template is the MNI152 template from the Montreal Neurological Institute. One this is done, the coordinates of a voxel are in the same space as the template and can be used to estimate its brain location using brain atlases based on tht same template.As already mentioned, the nistats package is not meant to perform spatial preprocessing, but only statistical analyses on the voxel times series, regardless of the coordinate system. +.. note:: Before fMRI images can be used to do meaningful comparisons, they must be processed to ensure that the voxels that are being compared represent the same brain regions, irrespective of the variability in size and shape of the brain and its microarchitecture across different subjects in the experiment. The process is called spatial registration or spatial normalization. During this procedure, the voxels of all the brain images are 'registered' to correspond to the same region of the brain. Usually, the images (their voxels) are registered to a standard 'template' brain image (its voxels). One often used standard template is the MNI152 template from the Montreal Neurological Institute. Once this is done, the coordinates of a voxel are in the same space as the template and can be used to estimate its brain location using brain atlases based on that same template. As already mentioned, the NiStats package is not meant to perform spatial preprocessing, but only statistical analyses on the voxel times series, regardless of the coordinate system. -FMRI data modeling ------------------- +fMRI data modelling +------------------- One way to analyze times series consists in comparing them to a *model* built from our knowledge of the events that occurred during the functional session. Events can correspond to actions of the participant (e.g. button presses), presentations of sensory stimui (e.g. sound, images), or hypothesized internal processes (e.g. memorization of a stimulus), ... @@ -47,16 +47,17 @@ One way to analyze times series consists in comparing them to a *model* built fr .. figure:: images/stimulation-time-diagram.png -One expects that a brain region involved in the processing of a certain type of event (e.g. the auditory cortex for sounds), would show a time course of activation that correlates with the time-diagram of these events. If the fMRI signal directly showed neural activity and did not contain any noise, we could just look at it in various voxel and detect those that conform to the time-diagrams. +One expects that a brain region involved in the processing of a certain type of event (e.g. the auditory cortex for sounds), would show a time course of activation that correlates with the time-diagram of these events. If the fMRI signal directly showed neural activity and did not contain any noise, we could just look at it in various voxels and detect those that conform to the time-diagrams. -Yet, we know, from previous measurements, that the BOLD signal does not follow the exact time course of stimulus processing and the underlying neural activity. The BOLD response reflects changes in blood flow and concentrations in oxy-deoxy hemoglobin, all together forming an `haemodynamic response`_ which is sluggish and long-lasting, as can be seen on the following figure showing the response to an impulsive event (for example, an auditory click played to the participants). +Yet, we know, from previous measurements, that the BOLD signal does not follow the exact time course of stimulus processing and the underlying neural activity. The BOLD response reflects changes in blood flow and concentrations in oxy-deoxy haemoglobin, all together forming a `haemodynamic response`_ which is sluggish and long-lasting, as can be seen on the following figure showing the response to an impulsive event (for example, an auditory click played to the participants). .. figure:: images/spm_iHRF.png -From the knowledge of the impulse haemodynamic response, we can build a predicted time course from the time-diagram of a type of event (The operation is known a convolution. Remark: it assumes linearity of the BOLD response, an assumption that may be wrong in some scenarios). It is this predicted time course, also known as a *predictor*, that is compared to the actual fMRI signal. If the correlation between the predictor and the signal is higher than expected by chance, the voxel is said to exhibit a significant response to the event type. +From the knowledge of the impulse haemodynamic response, we can build a predicted time course from the time-diagram of a type of event (The operation is known as `convolution`_. Simply stated, how the shape of one function's plot would affect the shape of another function's plot. **Remark:** it assumes linearity of the BOLD response, an assumption that may be wrong in some scenarios). It is this predicted time course, also known as a *predictor*, that is compared to the actual fMRI signal. If the correlation between the predictor and the signal is higher than expected by chance, the voxel is said to exhibit a significant response to the event type. -.. _a haemodynamic response: https://en.wikipedia.org/wiki/Haemodynamic_response +.. _haemodynamic response: https://en.wikipedia.org/wiki/Haemodynamic_response +.. _convolution: https://en.wikipedia.org/wiki/Convolution .. figure:: images/time-course-and-model-fit-in-a-voxel.png @@ -64,15 +65,15 @@ From the knowledge of the impulse haemodynamic response, we can build a predicte Correlations are computed separately at each voxel and a correlation map can be produced displaying the values of correlations (real numbers between -1 and +1) at each voxel. Generally, however, the maps presented in the papers report the significance of the correlations at each voxel, in forms of T, Z or p values for the null hypothesis test of no correlation (see below). For example, the following figure displays a Z-map showing voxels responding to auditory events. Large (positive or negative) Z values are unlikely to be due to chance alone. The map is tresholded so that only voxels with a p-value less than 1/1000 are coloured. .. note:: - Because, in this approach, hypothesis tests are conducted in parallel at many voxels, the likelihood of making false alarms is important. This is known as the problem of multiple comparisons. It is beyond the scope of this short notice to explain ways in which this problem can be approached. Let us just mention that this issue can be addressed in nistats by using random permutations tests. + In this approach, hypothesis tests are conducted in parallel at many voxels, increasing the liklelihood of False Positives. This is known as the problem of multiple comparisons. It is beyond the scope of this short notice to explain ways in which this problem can be approached. Let us just mention that this issue can be addressed in nistats by using random permutations tests. .. figure:: images/example-spmZ_map.png -In most fMRI experiments, several predictors are needed to fullly describe the events occuring during the session -- for example, the experimenter may want to distinguish brain activities linked to the perception of auditory stimuli or to button presses. To find the effect specific to each predictor, a multiple `linear regression`_ approach is typically used: all predictors are entered as columns in a *design-matrix* and the software finds the linear combination of these columns that best fits the signal. The weights assigned to each predictor by this linear combination are estimates of the contribution of this predictor to the response in the voxel. One can plot this effect size maps or, maps showing their statistical significance (how unlikely they are under the null hypothesis of no effect). +In most fMRI experiments, several predictors are needed to fully describe the events occuring during the session -- for example, the experimenter may want to distinguish brain activities linked to the perception of auditory stimuli or to button presses. To find the effect specific to each predictor, a multiple `linear regression`_ approach is typically used: all predictors are entered as columns in a *design-matrix* and the software finds the linear combination of these columns that best fits the signal. The weights assigned to each predictor by this linear combination are estimates of the contribution of this predictor to the response in the voxel. One can plot this using effect size maps or, maps showing their statistical significance (how unlikely they are under the null hypothesis of no effect). -.. _a linear regression: https://en.wikipedia.org/wiki/Linear_regression +.. _linear regression: https://en.wikipedia.org/wiki/Linear_regression In brief, the analysis of fMRI images involves: @@ -85,17 +86,17 @@ In brief, the analysis of fMRI images involves: fMRI statistical analysis ------------------------- -As put in the previous section, The basic statistical analysis of fMRI is conceptually a correlation analysis, where one seeks whether a certain combination (contrast) of columns of the design matrix fits a significant proportion of the fMRI signal at a given location. +As explained in the previous section, the basic statistical analysis of fMRI is conceptually a correlation analysis, where one seeks whether a certain combination (contrast) of columns of the design matrix fits a significant proportion of the fMRI signal at a given location. -It can be shown that this is equivalent to studying whether the estimated contrast effect is large with respect to the uncertainty about its exact value. Conretely, we compute the effect size estimate and the uncertainty about its value and divide the to. The resulting number has no physical dimension, it is a statistic --- A student or t-statistic, which we will denote `t`. +It can be shown that this is equivalent to studying whether the estimated contrast effect is large with respect to the uncertainty about its exact value. Concretely, we compute the effect size estimate and the uncertainty about its value and divide the two. The resulting number has no physical dimension, it is a statistic --- a Student or t-statistic, which we will denote `t`. Next, based on `t`, we want to decide whether the true effect was indeed greater than zero or not. -If the true effect were zero, `t` would not necessarily be 0: by chance, the noise in the data my be partly explained by the contrast of interest. -However, if we assume that the noise is Gaussian, and that the model is correctly specificed, then we know that `t` should follow a Student distribution with `dof` degrees of freedom, where q is the number of free parameters in the model: in practive, the number of observations (i.e. the number of time points), `n_scans` minus the number of effects modeled (i.e. the number of columns `n_columns`) of the design matrix: +If the true effect were zero, `t` would not necessarily be 0: by chance, the noise in the data may be partly explained by the contrast of interest. +However, if we assume that the noise is Gaussian, and that the model is correctly specified, then we know that `t` should follow a Student distribution with `dof` degrees of freedom, where q is the number of free parameters in the model: in practice, the number of observations (i.e. the number of time points), `n_scans` minus the number of effects modelled (i.e. the number of columns `n_columns`) of the design matrix: :math: `dof = n_scans - n_columns` -With this we can do statistical inference: Given a pre-defined error rate :math:`\alpha`, we compare the observed `t` to the :math:`(1-\alpha)` quantile of the Student distribution with `dof` degrees of freedom. If t is greater than this number, we can reject the null hypothesis with a *p-value* :math:`\alpha`, meaning: if there were no effect, the probability of oberving an effect as large as t would be less than `\alpha`. +With this we can do statistical inference: Given a pre-defined error rate :math:`\alpha`, we compare the observed `t` to the :math:`(1-\alpha)` quantile of the Student distribution with `dof` degrees of freedom. If t is greater than this number, we can reject the null hypothesis with a *p-value* :math:`\alpha`, meaning: if there were no effect, the probability of oberving an effect as large as `t` would be less than `\alpha`. .. figure:: images/student.png @@ -109,10 +110,10 @@ Multiple comparisons A well-known issue that arrives then is that of multiple comparisons: when a statistical tests is repeated a large number times, say one for each voxel, i.e. `n_voxels` times, then one can expect that, in the absence of any effect, the number of detections ---false detections since there is no effect--- will be roughly :math:`n\_voxels \alpha`. Then, take :math:`\alpha=.001` and :math:`n=10^5`, the number of false detections will be about 100. The danger is that one may no longer trust the detections, i.e. values of `z` larger than the :math:`(1-\alpha)`-quantile of the standard normal distribution. -The first idea that one might think of is to take a much smaller :math:`\alpha`: for instance, if we take, :math:`\alpha=\frac{0.05}{n\_voxels}` then the expected number of false discoveries is only about 0.05, meaning that there is a 5% chance to declare active a truly inactive voxel. This correction on the signifiance is known as Bonferroni procedure. It is fairly accurate when the different tesst are independent or close to independent, and becomes conservative otherwise. +The first idea that one might think of is to take a much smaller :math:`\alpha`: for instance, if we take, :math:`\alpha=\frac{0.05}{n\_voxels}` then the expected number of false discoveries is only about 0.05, meaning that there is a 5% chance to declare active a truly inactive voxel. This correction on the signifiance is known as Bonferroni procedure. It is fairly accurate when the different tests are independent or close to independent, and becomes conservative otherwise. The problem with his approach is that truly activated voxel may not surpass the corresponding threshold, which is typically very high, because `n\_voxels` is large. -A second possibility is to choose a threshold so that the proportion of true discoveries among the discoveries reaches a certain proportion `0 Date: Thu, 15 Nov 2018 19:12:12 +0100 Subject: [PATCH 184/210] Fixed: Multiple Comparisons reflink+ nistats, nipype in CamelCase --- doc/introduction.rst | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/doc/introduction.rst b/doc/introduction.rst index b24ebfb3..8ea07f36 100644 --- a/doc/introduction.rst +++ b/doc/introduction.rst @@ -1,5 +1,5 @@ ===================================== -Introduction: nistats in a nutshell +Introduction: NiStats in a nutshell ===================================== .. contents:: **Contents** @@ -7,18 +7,18 @@ Introduction: nistats in a nutshell :depth: 1 -What is nistats? +What is NiStats? =========================================================================== -.. topic:: **What is nistats?** +.. topic:: **What is NiStats?** - Nistats is a Python module to perform voxel-wise analyses of functional magnetic resonance images (fMRI) using linear models. It provides functions to create design matrices, at the subject and group levels, to estimate them from images series and to compute statistical maps (contrasts). It allows to perform the same statistical analyses as `SPM`_ or `FSL`_ (but it does not provide tools for preprocessing stages (realignment, spatial normalization, etc.); for this, see `nipype`_. + Nistats is a Python module to perform voxel-wise analyses of functional magnetic resonance images (fMRI) using linear models. It provides functions to create design matrices, at the subject and group levels, to estimate them from images series and to compute statistical maps (contrasts). It allows to perform the same statistical analyses as `SPM`_ or `FSL`_ (but it does not provide tools for preprocessing stages (realignment, spatial normalization, etc.); for this, see `NiPype`_. .. _SPM: https://www.fil.ion.ucl.ac.uk/spm/ .. _FSL: https://www.fmrib.ox.ac.uk/fsl -.. _nipype: https://nipype.readthedocs.io/en/latest/ +.. _NiPype: https://nipype.readthedocs.io/en/latest/ @@ -65,7 +65,7 @@ From the knowledge of the impulse haemodynamic response, we can build a predicte Correlations are computed separately at each voxel and a correlation map can be produced displaying the values of correlations (real numbers between -1 and +1) at each voxel. Generally, however, the maps presented in the papers report the significance of the correlations at each voxel, in forms of T, Z or p values for the null hypothesis test of no correlation (see below). For example, the following figure displays a Z-map showing voxels responding to auditory events. Large (positive or negative) Z values are unlikely to be due to chance alone. The map is tresholded so that only voxels with a p-value less than 1/1000 are coloured. .. note:: - In this approach, hypothesis tests are conducted in parallel at many voxels, increasing the liklelihood of False Positives. This is known as the problem of multiple comparisons. It is beyond the scope of this short notice to explain ways in which this problem can be approached. Let us just mention that this issue can be addressed in nistats by using random permutations tests. + In this approach, hypothesis tests are conducted in parallel at many voxels, increasing the liklelihood of False Positives. This is known as the Problem of `Multiple Comparisons`_. Some common strategies for dealing with this are discussed later in this page. This issue can also be addressed in NiStats by using random permutations tests. .. figure:: images/example-spmZ_map.png @@ -104,7 +104,7 @@ With this we can do statistical inference: Given a pre-defined error rate :math: .. note:: It is cumbersome to work with Student distributions, since those always require to specify the number `dof` of degrees of freedom. To avoid this, we can transform `t` to another variable `z` such that comparing `t` to the Student distribution with `dof` degrees of freedom is equivalent to comparing `z` to a standard normal distribution. We call this a z-transform of `t`. We call the :math:`(1-\alpha)` quantile of the normal distribution the *threshold*, since we use this value to declare voxels active or not. -Multiple comparisons +Multiple Comparisons -------------------- A well-known issue that arrives then is that of multiple comparisons: @@ -128,7 +128,7 @@ Tutorials .. _installation: -Installing nistats +Installing NiStats ==================== .. raw:: html From 70b8caf1fec7b6302fb879ffae203355937edbc1 Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Fri, 16 Nov 2018 09:59:55 +0100 Subject: [PATCH 185/210] Corrected: Replaced NiStats with Nistats --- doc/introduction.rst | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/doc/introduction.rst b/doc/introduction.rst index 8ea07f36..47dcbe40 100644 --- a/doc/introduction.rst +++ b/doc/introduction.rst @@ -1,5 +1,5 @@ ===================================== -Introduction: NiStats in a nutshell +Introduction: Nistats in a nutshell ===================================== .. contents:: **Contents** @@ -7,10 +7,10 @@ Introduction: NiStats in a nutshell :depth: 1 -What is NiStats? -=========================================================================== +What is Nistats? +================ -.. topic:: **What is NiStats?** +.. topic:: **What is Nistats?** Nistats is a Python module to perform voxel-wise analyses of functional magnetic resonance images (fMRI) using linear models. It provides functions to create design matrices, at the subject and group levels, to estimate them from images series and to compute statistical maps (contrasts). It allows to perform the same statistical analyses as `SPM`_ or `FSL`_ (but it does not provide tools for preprocessing stages (realignment, spatial normalization, etc.); for this, see `NiPype`_. @@ -36,7 +36,7 @@ A cerebral MR image provides a 3D image of the brain that can be decomposed into .. _voxels: https://en.wikipedia.org/wiki/Voxel -.. note:: Before fMRI images can be used to do meaningful comparisons, they must be processed to ensure that the voxels that are being compared represent the same brain regions, irrespective of the variability in size and shape of the brain and its microarchitecture across different subjects in the experiment. The process is called spatial registration or spatial normalization. During this procedure, the voxels of all the brain images are 'registered' to correspond to the same region of the brain. Usually, the images (their voxels) are registered to a standard 'template' brain image (its voxels). One often used standard template is the MNI152 template from the Montreal Neurological Institute. Once this is done, the coordinates of a voxel are in the same space as the template and can be used to estimate its brain location using brain atlases based on that same template. As already mentioned, the NiStats package is not meant to perform spatial preprocessing, but only statistical analyses on the voxel times series, regardless of the coordinate system. +.. note:: Before fMRI images can be used to do meaningful comparisons, they must be processed to ensure that the voxels that are being compared represent the same brain regions, irrespective of the variability in size and shape of the brain and its microarchitecture across different subjects in the experiment. The process is called spatial registration or spatial normalization. During this procedure, the voxels of all the brain images are 'registered' to correspond to the same region of the brain. Usually, the images (their voxels) are registered to a standard 'template' brain image (its voxels). One often used standard template is the MNI152 template from the Montreal Neurological Institute. Once this is done, the coordinates of a voxel are in the same space as the template and can be used to estimate its brain location using brain atlases based on that same template. As already mentioned, the Nistats package is not meant to perform spatial preprocessing, but only statistical analyses on the voxel times series, regardless of the coordinate system. fMRI data modelling ------------------- @@ -65,7 +65,7 @@ From the knowledge of the impulse haemodynamic response, we can build a predicte Correlations are computed separately at each voxel and a correlation map can be produced displaying the values of correlations (real numbers between -1 and +1) at each voxel. Generally, however, the maps presented in the papers report the significance of the correlations at each voxel, in forms of T, Z or p values for the null hypothesis test of no correlation (see below). For example, the following figure displays a Z-map showing voxels responding to auditory events. Large (positive or negative) Z values are unlikely to be due to chance alone. The map is tresholded so that only voxels with a p-value less than 1/1000 are coloured. .. note:: - In this approach, hypothesis tests are conducted in parallel at many voxels, increasing the liklelihood of False Positives. This is known as the Problem of `Multiple Comparisons`_. Some common strategies for dealing with this are discussed later in this page. This issue can also be addressed in NiStats by using random permutations tests. + In this approach, hypothesis tests are conducted in parallel at many voxels, increasing the liklelihood of False Positives. This is known as the Problem of `Multiple Comparisons`_. Some common strategies for dealing with this are discussed later in this page. This issue can also be addressed in Nistats by using random permutations tests. .. figure:: images/example-spmZ_map.png @@ -128,8 +128,8 @@ Tutorials .. _installation: -Installing NiStats -==================== +Installing Nistats +================== .. raw:: html :file: install_doc_component.html From c91bd7c3703534712600446be536f329dc94230e Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Mon, 19 Nov 2018 17:14:08 +0100 Subject: [PATCH 186/210] Corrected some RestructuredText misformatting & a typo --- examples/01_tutorials/plot_bids_analysis.py | 5 +++-- .../01_tutorials/plot_single_subject_single_run.py | 6 ++---- examples/02_first_level_models/plot_adhd_dmn.py | 5 ++--- .../plot_localizer_surface_analysis.py | 12 ++++-------- .../plot_second_level_two_sample_test.py | 3 +-- 5 files changed, 12 insertions(+), 19 deletions(-) diff --git a/examples/01_tutorials/plot_bids_analysis.py b/examples/01_tutorials/plot_bids_analysis.py index 1bc01201..784fdf85 100644 --- a/examples/01_tutorials/plot_bids_analysis.py +++ b/examples/01_tutorials/plot_bids_analysis.py @@ -84,11 +84,11 @@ # ---------------------------- # Now we simply fit each first level model and plot for each subject the # contrast that reveals the language network (language - string). Notice that -# we can define a contrast using the names of the conditions especified in the +# we can define a contrast using the names of the conditions specified in the # events dataframe. Sum, substraction and scalar multiplication are allowed. ############################################################################ -# set the threshold as the z-variate with an uncorrected p-value of 0.001 +# Set the threshold as the z-variate with an uncorrected p-value of 0.001 from scipy.stats import norm p001_unc = norm.isf(0.001) @@ -141,3 +141,4 @@ title='Group language network (unc p<0.001)', plot_abs=False, display_mode='x') plotting.show() + diff --git a/examples/01_tutorials/plot_single_subject_single_run.py b/examples/01_tutorials/plot_single_subject_single_run.py index dafe2535..73c22489 100644 --- a/examples/01_tutorials/plot_single_subject_single_run.py +++ b/examples/01_tutorials/plot_single_subject_single_run.py @@ -87,10 +87,8 @@ # # * t_r=7(s) is the time of repetition of acquisitions # * noise_model='ar1' specifies the noise covariance model: a lag-1 dependence -# * standardize=False means that we do not want to rescale the time -# series to mean 0, variance 1 -# * hrf_model='spm' means that we rely on the SPM "canonical hrf" -# model (without time or dispersion derivatives) +# * standardize=False means that we do not want to rescale the time series to mean 0, variance 1 +# * hrf_model='spm' means that we rely on the SPM "canonical hrf" model (without time or dispersion derivatives) # * drift_model='cosine' means that we model the signal drifts as slow oscillating time functions # * period_cut=160(s) defines the cutoff frequency (its inverse actually). fmri_glm = FirstLevelModel(t_r=7, diff --git a/examples/02_first_level_models/plot_adhd_dmn.py b/examples/02_first_level_models/plot_adhd_dmn.py index 20d8739f..54cb9b44 100644 --- a/examples/02_first_level_models/plot_adhd_dmn.py +++ b/examples/02_first_level_models/plot_adhd_dmn.py @@ -9,9 +9,8 @@ 1. A sequence of fMRI volumes are loaded 2. A design matrix with the Posterior Cingulate Cortex seed is defined -4. A GLM is applied to the dataset (effect/covariance, - then contrast estimation) -5. The Default Mode Network is displayed +3. A GLM is applied to the dataset (effect/covariance, then contrast estimation) +4. The Default Mode Network is displayed """ import numpy as np diff --git a/examples/02_first_level_models/plot_localizer_surface_analysis.py b/examples/02_first_level_models/plot_localizer_surface_analysis.py index af702401..bdabe0f3 100644 --- a/examples/02_first_level_models/plot_localizer_surface_analysis.py +++ b/examples/02_first_level_models/plot_localizer_surface_analysis.py @@ -7,11 +7,9 @@ More specifically: 1. A sequence of fMRI volumes are loaded -2. fMRI data are projected onto a reference cortical surface (the -freesurfer template, fsaverage) +2. fMRI data are projected onto a reference cortical surface (the freesurfer template, fsaverage) 3. A design matrix describing all the effects related to the data is computed -4. A GLM is applied to the dataset (effect/covariance, - then contrast estimation) +4. A GLM is applied to the dataset (effect/covariance, then contrast estimation) The result of the analysis are statistical maps that are defined on the brain mesh. We display them using Nilearn capabilities. @@ -19,11 +17,9 @@ The projection of fMRI data onto a given brain mesh requires that both are initially defined in the same space. -* The functional data should be coregistered to the anatomy from which - the mesh was obtained. +* The functional data should be coregistered to the anatomy from which the mesh was obtained. -* Another possibility, used here, is to project the normalized fMRI - data to an MNI-coregistered mesh, such as fsaverage. +* Another possibility, used here, is to project the normalized fMRI data to an MNI-coregistered mesh, such as fsaverage. The advantage of this second approach is that it makes it easy to run second-level analyses on the surface. On the other hand, it is diff --git a/examples/03_second_level_models/plot_second_level_two_sample_test.py b/examples/03_second_level_models/plot_second_level_two_sample_test.py index 3e5eb391..7b6ccc22 100644 --- a/examples/03_second_level_models/plot_second_level_two_sample_test.py +++ b/examples/03_second_level_models/plot_second_level_two_sample_test.py @@ -7,8 +7,7 @@ More specifically: 1. A sample of n=16 visual activity fMRIs are downloaded. -2. A two-sample t-test is applied to the brain maps in order to see -the effect of the contrast difference across subjects. +2. A two-sample t-test is applied to the brain maps in order to see the effect of the contrast difference across subjects. The contrast is between responses to vertical versus horizontal checkerboards than are retinotopically distinct. At the individual From 69556d35e0678adc3f80467f6f27b930b0a45b27 Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Mon, 19 Nov 2018 18:59:04 +0100 Subject: [PATCH 187/210] Corrected many RestructuredText misformatting in docstrings --- doc/index.rst | 2 +- nistats/design_matrix.py | 15 +++++++++------ nistats/experimental_paradigm.py | 2 ++ nistats/first_level_model.py | 9 ++++++--- nistats/hemodynamic_models.py | 13 +++++++------ nistats/model.py | 6 ++++-- nistats/reporting.py | 4 +++- nistats/second_level_model.py | 5 ++++- nistats/thresholding.py | 6 +++--- nistats/utils.py | 3 ++- 10 files changed, 41 insertions(+), 24 deletions(-) diff --git a/doc/index.rst b/doc/index.rst index 77c128d3..384e0e0c 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -19,7 +19,7 @@ .. |design_matrix| image:: auto_examples/04_low_level_functions/images/thumb/sphx_glr_plot_design_matrix_thumb.png :target: auto_examples/04_low_level_functions/plot_design_matrix.html -.. |first_level| image:: auto_examples/02_first_level_models/images/thumb/sphx_glr_plot_localizer_analysis_thumb.png +.. |first_level| image:: auto_examples/02_first_level_models/images/thumb/sphx_glr_plot_localizer_surface_analysis_thumb.png :target: auto_examples/02_first_level_models/plot_localizer_analysis.html .. |second_level| image:: auto_examples/03_second_level_models/images/thumb/sphx_glr_plot_thresholding_thumb.png diff --git a/nistats/design_matrix.py b/nistats/design_matrix.py index 75164ef6..7f461b22 100644 --- a/nistats/design_matrix.py +++ b/nistats/design_matrix.py @@ -13,14 +13,15 @@ 1. Task-related regressors, that result from the convolution of the experimental paradigm regressors with hemodynamic models A hemodynamic model is one of: - 'spm' : linear filter used in the SPM software - 'glover' : linear filter estimated by G.Glover - 'spm + derivative', 'glover + derivative': the same linear models, + + - 'spm' : linear filter used in the SPM software + - 'glover' : linear filter estimated by G.Glover + - 'spm + derivative', 'glover + derivative': the same linear models, plus their time derivative (2 regressors per condition) - 'spm + derivative + dispersion', 'glover + derivative + dispersion': + - 'spm + derivative + dispersion', 'glover + derivative + dispersion': idem plus the derivative wrt the dispersion parameter of the hrf (3 regressors per condition) - 'fir' : finite impulse response model, generic linear filter + - 'fir' : finite impulse response model, generic linear filter 2. User-specified regressors, that represent information available on the data, e.g. motion parameters, physiological data resampled at @@ -32,6 +33,7 @@ estimates. Author: Bertrand Thirion, 2009-2015 + """ from __future__ import with_statement from warnings import warn @@ -291,7 +293,7 @@ def make_first_level_design_matrix( events : DataFrame instance, optional Events data that describes the experimental paradigm. - The DataFrame instance might have these keys: + The DataFrame instance might have these keys: 'onset': column to specify the start time of each events in seconds. An error is raised if this key is missing. 'trial_type': column to specify per-event experimental conditions @@ -303,6 +305,7 @@ def make_first_level_design_matrix( 'modulation': column to specify the amplitude of each events. If missing the default is set to ones(n_events). + An experimental paradigm is valid if it has an 'onset' key and a 'duration' key. If these keys are missing an error will be raised. diff --git a/nistats/experimental_paradigm.py b/nistats/experimental_paradigm.py index 34ec3805..6f8e8c51 100644 --- a/nistats/experimental_paradigm.py +++ b/nistats/experimental_paradigm.py @@ -6,12 +6,14 @@ This yields the onset time of the events in the experimental paradigm. It can also contain: + * a 'trial_type' field that yields the condition identifier. * a 'duration' field that yields event duration (for so-called block paradigms). * a 'modulation' field that associated a scalar value to each event. Author: Bertrand Thirion, 2015 + """ from __future__ import with_statement import numpy as np diff --git a/nistats/first_level_model.py b/nistats/first_level_model.py index 767bede8..a9a3de94 100644 --- a/nistats/first_level_model.py +++ b/nistats/first_level_model.py @@ -322,13 +322,15 @@ def fit(self, run_imgs, events=None, confounds=None, the affine is considered the same for all. events: pandas Dataframe or string or list of pandas DataFrames or - strings, + strings + fMRI events used to build design matrices. One events object expected per run_img. Ignored in case designs is not None. If string, then a path to a csv file is expected. confounds: pandas Dataframe or string or list of pandas DataFrames or - strings, + strings + Each column in a DataFrame corresponds to a confound variable to be included in the regression model of the respective run_img. The number of rows must match the number of volumes in the @@ -497,13 +499,14 @@ def compute_contrast(self, contrast_def, stat_type=None, ---------- contrast_def : str or array of shape (n_col) or list of (string or array of shape (n_col)) + where ``n_col`` is the number of columns of the design matrix, (one array per run). If only one array is provided when there are several runs, it will be assumed that the same contrast is desired for all runs. The string can be a formula compatible with the linear constraint of the Patsy library. Basically one can use the name of the conditions as they appear in the design matrix of - the fitted model combined with operators /*+- and numbers. + the fitted model combined with operators /\*+- and numbers. Please checks the patsy documentation for formula examples: http://patsy.readthedocs.io/en/latest/API-reference.html#patsy.DesignInfo.linear_constraint diff --git a/nistats/hemodynamic_models.py b/nistats/hemodynamic_models.py index b20c13ec..25e945a2 100644 --- a/nistats/hemodynamic_models.py +++ b/nistats/hemodynamic_models.py @@ -472,16 +472,17 @@ def compute_regressor(exp_condition, hrf_model, frame_times, con_id='cond', Notes ----- The different hemodynamic models can be understood as follows: - 'spm': this is the hrf model used in SPM - 'spm + derivative': SPM model plus its time derivative (2 regressors) - 'spm + time + dispersion': idem, plus dispersion derivative (3 regressors) - 'glover': this one corresponds to the Glover hrf - 'glover + derivative': the Glover hrf + time derivative (2 regressors) - 'glover + derivative + dispersion': idem + dispersion derivative + - 'spm': this is the hrf model used in SPM + - 'spm + derivative': SPM model plus its time derivative (2 regressors) + - 'spm + time + dispersion': idem, plus dispersion derivative (3 regressors) + - 'glover': this one corresponds to the Glover hrf + - 'glover + derivative': the Glover hrf + time derivative (2 regressors) + - 'glover + derivative + dispersion': idem + dispersion derivative (3 regressors) 'fir': finite impulse response basis, a set of delayed dirac models with arbitrary length. This one currently assumes regularly spaced frame times (i.e. fixed time of repetition). + It is expected that spm standard and Glover model would not yield large differences in most cases. diff --git a/nistats/model.py b/nistats/model.py index 140e507d..a8bbfd79 100644 --- a/nistats/model.py +++ b/nistats/model.py @@ -300,10 +300,12 @@ def conf_int(self, alpha=.05, cols=None, dispersion=None): Notes ----- + Confidence intervals are two-tailed. - TODO: + tails : string, optional - `tails` can be "two", "upper", or "lower" + Possible values: 'two' | 'upper' | 'lower' + ''' if cols is None: lower = self.theta - inv_t_cdf(1 - alpha / 2, self.df_resid) *\ diff --git a/nistats/reporting.py b/nistats/reporting.py index 10fc4813..303d6719 100644 --- a/nistats/reporting.py +++ b/nistats/reporting.py @@ -328,13 +328,14 @@ def plot_contrast_matrix(contrast_def, design_matrix, colorbar=False, ax=None, ---------- contrast_def : str or array of shape (n_col) or list of (string or array of shape (n_col)) + where ``n_col`` is the number of columns of the design matrix, (one array per run). If only one array is provided when there are several runs, it will be assumed that the same contrast is desired for all runs. The string can be a formula compatible with the linear constraint of the Patsy library. Basically one can use the name of the conditions as they appear in the design matrix of - the fitted model combined with operators /*+- and numbers. + the fitted model combined with operators /\*+- and numbers. Please checks the patsy documentation for formula examples: http://patsy.readthedocs.io/en/latest/API-reference.html#patsy.DesignInfo.linear_constraint @@ -355,6 +356,7 @@ def plot_contrast_matrix(contrast_def, design_matrix, colorbar=False, ax=None, Returns ------- Plot Axes object + """ design_column_names = design_matrix.columns.tolist() diff --git a/nistats/second_level_model.py b/nistats/second_level_model.py index aa1996f0..f53883d6 100644 --- a/nistats/second_level_model.py +++ b/nistats/second_level_model.py @@ -124,6 +124,7 @@ def fit(self, second_level_input, confounds=None, design_matrix=None): ---------- second_level_input: list of `FirstLevelModel` objects or pandas DataFrame or list of Niimg-like objects. + Giving FirstLevelModel objects will allow to easily compute the second level contast of arbitrary first level contrasts thanks to the first_level_contrast argument of the compute_contrast @@ -156,6 +157,7 @@ def fit(self, second_level_input, confounds=None, design_matrix=None): from second_level_input. Ensure that the order of maps given by a second_level_input list of Niimgs matches the order of the rows in the design matrix. + """ # Check parameters # check first level input @@ -325,7 +327,7 @@ def compute_contrast( The string can be a formula compatible with the linear constraint of the Patsy library. Basically one can use the name of the conditions as they appear in the design matrix of - the fitted model combined with operators /*+- and numbers. + the fitted model combined with operators /\*+- and numbers. Please check the patsy documentation for formula examples: http://patsy.readthedocs.io/en/latest/API-reference.html#patsy.DesignInfo.linear_constraint The default (None) is accepted if the design matrix has a single @@ -335,6 +337,7 @@ def compute_contrast( first_level_contrast: str or array of shape (n_col) with respect to FirstLevelModel, optional + In case a list of FirstLevelModel was provided as second_level_input, we have to provide a contrast to apply to the first level models to get the corresponding list of images diff --git a/nistats/thresholding.py b/nistats/thresholding.py index 672aa474..cb4c8df8 100644 --- a/nistats/thresholding.py +++ b/nistats/thresholding.py @@ -55,13 +55,13 @@ def map_threshold(stat_img=None, mask_img=None, level=.001, level: float, optional number controling the thresholding (either a p-value or z-scale value). Not to be confused with the z-scale threshold: level can be a p-values, - e.g. "0.05" or another type of number depending on the height_ - control parameter. The z-scale threshold is actually returned by + e.g. "0.05" or another type of number depending on the + height_control parameter. The z-scale threshold is actually returned by the function. height_control: string, or None optional false positive control meaning of cluster forming - threshold: 'fpr'|'fdr'|'bonferroni'|None + threshold: 'fpr'|'fdr'|'bonferroni'\|None cluster_threshold : float, optional cluster size threshold. In the returned thresholded map, diff --git a/nistats/utils.py b/nistats/utils.py index 50105c7a..b6ff672d 100644 --- a/nistats/utils.py +++ b/nistats/utils.py @@ -394,13 +394,14 @@ def parse_bids_filename(img_path): 'file_tag', 'file_type' and 'file_fields'. The 'file_tag' field refers to the last part of the file under the - BIDS convention that is of the form *_tag.type. Contrary to the rest + BIDS convention that is of the form \*_tag.type. Contrary to the rest of the file name it is not a key-value pair. This notion should be revised in the case we are handling derivatives since so far the convention will keep the tag prepended to any fields added in the case of preprocessed files that also end with another tag. This parser will consider any tag in the middle of the file name as a key with no value and will be included in the 'file_fields' key. + """ reference = {} reference['file_path'] = img_path From 703e02068c6c594d94f74b1f603c5a7bb7af0532 Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Wed, 21 Nov 2018 15:33:53 +0100 Subject: [PATCH 188/210] Adding entry for newly relocated example in carousel section --- doc/index.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/index.rst b/doc/index.rst index 384e0e0c..b65a0449 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -25,8 +25,8 @@ .. |second_level| image:: auto_examples/03_second_level_models/images/thumb/sphx_glr_plot_thresholding_thumb.png :target: auto_examples/03_second_level_models/plot_thresholding.html -.. |bids| image:: auto_examples/01_tutorials/images/thumb/sphx_glr_plot_bids_analysis_thumb.png - :target: auto_examples/01_tutorials/plot_bids_analysis.html +.. |bids| image:: auto_examples/05_complete_examples/images/thumb/sphx_glr_plot_bids_analysis_thumb.png + :target: auto_examples/05_complete_examples/plot_bids_analysis.html .. raw:: html From 6559776adb264b06239a6afbe67dc7c590638c91 Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Wed, 21 Nov 2018 18:02:23 +0100 Subject: [PATCH 189/210] Adding README.txt to new example dir, necessary for example generation - Sphinx gallery looks for a README.txt file in each directory where an example has to be generated. - Typo in carousel fixed. --- doc/index.rst | 2 +- examples/05_complete_examples/README.txt | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 examples/05_complete_examples/README.txt diff --git a/doc/index.rst b/doc/index.rst index b65a0449..9878b7cc 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -11,7 +11,7 @@ scientific stack like Scipy, Numpy and Pandas. -.. Here we are building the carrousel +.. Here we are building the carousel .. |hrf| image:: auto_examples/04_low_level_functions/images/thumb/sphx_glr_plot_hrf_thumb.png :target: auto_examples/04_low_level_functions/plot_hrf.html diff --git a/examples/05_complete_examples/README.txt b/examples/05_complete_examples/README.txt new file mode 100644 index 00000000..3cc1e060 --- /dev/null +++ b/examples/05_complete_examples/README.txt @@ -0,0 +1,4 @@ +Complete examples +----------------- + +These are examples focused on showcasing complete step-by-step examples, including FIrst and Second Level Models. From e01df48a097f5f4cf06e4fee0d11a793f1588d79 Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Thu, 22 Nov 2018 15:54:48 +0100 Subject: [PATCH 190/210] Fixed many ore RestructuredText misformatting, corrected some text & typos --- .../01_tutorials/plot_first_level_model_details.py | 12 ++++++++---- examples/05_complete_examples/README.txt | 2 +- .../plot_surface_bids_analysis.py | 3 +-- nistats/reporting.py | 14 +++++++------- 4 files changed, 17 insertions(+), 14 deletions(-) diff --git a/examples/01_tutorials/plot_first_level_model_details.py b/examples/01_tutorials/plot_first_level_model_details.py index 53af77b7..64927f8c 100644 --- a/examples/01_tutorials/plot_first_level_model_details.py +++ b/examples/01_tutorials/plot_first_level_model_details.py @@ -1,5 +1,5 @@ """Studying first-level-model details in a trials-and-error fashion -================================================================ +=================================================================== In this tutorial, we study the parametrization of the first-level model used for fMRI data analysis and clarify their impact on the @@ -10,7 +10,8 @@ resulting brain maps. Readers without prior experience in fMRI data analysis should first -run the :ref:`plot_single_subject_single_run` tutorial to get a bit more +run the `Analysis of a single session, single subject fMRI dataset`_ +tutorial to get a bit more familiar with the base concepts, and only then run this tutorial example. To run this example, you must launch IPython via ``ipython @@ -20,6 +21,8 @@ :local: :depth: 1 +.. _Analysis of a single session, single subject fMRI dataset: plot_single_subject_single_run.html + """ ############################################################################### @@ -357,9 +360,10 @@ def plot_contrast(first_level_model): # is to estimate confounding effects from the data themselves, using # the compcorr approach, and take those into account in the model. # -# For this we rely on the so-called :ref:`high_variance_confounds -# ` +# For this we rely on the so-called `high_variance_confounds`_ # routine of Nilearn. +# +# .. _high_variance_confounds: https://nilearn.github.io/modules/generated/nilearn.image.high_variance_confounds.html from nilearn.image import high_variance_confounds diff --git a/examples/05_complete_examples/README.txt b/examples/05_complete_examples/README.txt index 3cc1e060..398942b8 100644 --- a/examples/05_complete_examples/README.txt +++ b/examples/05_complete_examples/README.txt @@ -1,4 +1,4 @@ Complete examples ----------------- -These are examples focused on showcasing complete step-by-step examples, including FIrst and Second Level Models. +Complete step-by-step examples, including First and Second Level Models. diff --git a/examples/05_complete_examples/plot_surface_bids_analysis.py b/examples/05_complete_examples/plot_surface_bids_analysis.py index 2671f057..9f5f2b82 100644 --- a/examples/05_complete_examples/plot_surface_bids_analysis.py +++ b/examples/05_complete_examples/plot_surface_bids_analysis.py @@ -8,8 +8,7 @@ More specifically: 1. Download an fMRI BIDS dataset with two language conditions to contrast. -2. Project the data to a standard mesh, fsaverage5, aka the Freesurfer -template mesh downsampled to about 10k nodes per hemisphere. +2. Project the data to a standard mesh, fsaverage5, aka the Freesurfer template mesh downsampled to about 10k nodes per hemisphere. 3. Run the first level model objects 4. Fit a second level model on the fitted first level models. diff --git a/nistats/reporting.py b/nistats/reporting.py index 303d6719..3f0e5dff 100644 --- a/nistats/reporting.py +++ b/nistats/reporting.py @@ -30,15 +30,15 @@ def _local_max(data, affine, min_distance): data : array_like 3D array of with masked values for cluster. - min_distance : :obj:`int` + min_distance : `int` Minimum distance between local maxima in ``data``, in terms of mm. Returns ------- - ijk : :obj:`numpy.ndarray` + ijk : `numpy.ndarray` (n_foci, 3) array of local maxima indices for cluster. - vals : :obj:`numpy.ndarray` + vals : `numpy.ndarray` (n_foci,) array of values from data at ijk. """ # Initial identification of subpeaks with minimal minimum distance @@ -90,19 +90,19 @@ def get_clusters_table(stat_img, stat_threshold, cluster_threshold=None, stat_img : Niimg-like object, Statistical image (presumably in z- or p-scale). - stat_threshold: :obj:`float` + stat_threshold: `float` Cluster forming threshold in same scale as `stat_img` (either a p-value or z-scale value). - cluster_threshold : :obj:`int` or :obj:`None`, optional + cluster_threshold : `int` or `None`, optional Cluster size threshold, in voxels. - min_distance: :obj:`float`, optional + min_distance: `float`, optional Minimum distance between subpeaks in mm. Default is 8 mm. Returns ------- - df : :obj:`pandas.DataFrame` + df : `pandas.DataFrame` Table with peaks and subpeaks from thresholded `stat_img`. For binary clusters (clusters with >1 voxel containing only one value), the table reports the center of mass of the cluster, rather than any peaks/subpeaks. From 308fd415719c1c5d87b1aa7261debd02dbdf9e08 Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Thu, 22 Nov 2018 16:19:24 +0100 Subject: [PATCH 191/210] Re-enabled `make html-strict` --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 253b648a..ddbef3d5 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -42,7 +42,7 @@ jobs: command: | source activate testenv pip install -e . - set -o pipefail && cd doc && make html 2>&1 | tee ~/log.txt + set -o pipefail && cd doc && make html-strict 2>&1 | tee ~/log.txt no_output_timeout: 5h - save_cache: key: v1-packages+datasets-{{ .Branch }} From 6caf4bf7504e82285a77f33fcbb8c08324f33698 Mon Sep 17 00:00:00 2001 From: btnguyen Date: Sun, 25 Nov 2018 21:28:18 +0100 Subject: [PATCH 192/210] first_level_model test case - memory caching computation --- nistats/tests/test_first_level_model.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/nistats/tests/test_first_level_model.py b/nistats/tests/test_first_level_model.py index ef834807..d3e7ff7f 100644 --- a/nistats/tests/test_first_level_model.py +++ b/nistats/tests/test_first_level_model.py @@ -277,6 +277,24 @@ def test_first_level_model_glm_computation(): # assert_equal(len(results1), len(results2)) ####FIX +def test_first_level_glm_computation_with_memory_caching(): + with InTemporaryDirectory(): + shapes = ((7, 8, 9, 10),) + mask, FUNCFILE, _ = write_fake_fmri_data(shapes) + FUNCFILE = FUNCFILE[0] + func_img = load(FUNCFILE) + # initialize FirstLevelModel with memory option enabled + t_r = 10.0 + slice_time_ref = 0. + events = basic_paradigm() + # ols case + model = FirstLevelModel(t_r, slice_time_ref, mask=mask, + drift_model='polynomial', drift_order=3, + memory='nilearn_cache', memory_level=1, + minimize_memory=False) + model.fit(func_img, events) + + def test_first_level_model_contrast_computation(): with InTemporaryDirectory(): shapes = ((7, 8, 9, 10),) From f109278b8d3140d4dee6456f5a2b137f2551f0fc Mon Sep 17 00:00:00 2001 From: btnguyen Date: Sun, 25 Nov 2018 21:28:47 +0100 Subject: [PATCH 193/210] rename test case for clarification --- nistats/tests/test_second_level_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nistats/tests/test_second_level_model.py b/nistats/tests/test_second_level_model.py index c4f5739b..cbd8f1b1 100644 --- a/nistats/tests/test_second_level_model.py +++ b/nistats/tests/test_second_level_model.py @@ -207,7 +207,7 @@ def test_second_level_model_contrast_computation(): assert_raises(ValueError, model.compute_contrast, None) -def test_second_level_model_contrast_computation_with_mem(): +def test_second_level_model_contrast_computation_with_memory_caching(): with InTemporaryDirectory(): shapes = ((7, 8, 9, 1),) mask, FUNCFILE, _ = write_fake_fmri_data(shapes) From 581bcb4aeb297fda67d60ca2f3f242aeb3bfd0ff Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Mon, 26 Nov 2018 13:45:22 -0500 Subject: [PATCH 194/210] FIX: numpy.linspace parameter num must be integer --- nistats/hemodynamic_models.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/nistats/hemodynamic_models.py b/nistats/hemodynamic_models.py index b20c13ec..bb82c385 100644 --- a/nistats/hemodynamic_models.py +++ b/nistats/hemodynamic_models.py @@ -52,7 +52,7 @@ def _gamma_difference_hrf(tr, oversampling=50, time_length=32., onset=0., hrf sampling on the oversampled time grid """ dt = tr / oversampling - time_stamps = np.linspace(0, time_length, float(time_length) / dt) + time_stamps = np.linspace(0, time_length, np.ceil(float(time_length) / dt).astype(np.int)) time_stamps -= onset hrf = gamma.pdf(time_stamps, delay / dispersion, dt / dispersion) -\ ratio * gamma.pdf( @@ -265,7 +265,8 @@ def _sample_condition(exp_condition, frame_times, oversampling=50, min_onset) * oversampling) + 1 hr_frame_times = np.linspace(frame_times.min() + min_onset, - frame_times.max() * (1 + 1. / (n - 1)), n_hr) + frame_times.max() * (1 + 1. / (n - 1)), + np.ceil(n_hr).astype(np.int)) # Get the condition information onsets, durations, values = tuple(map(np.asanyarray, exp_condition)) From 3c47c1a6148bbb2c5d72f8c4b4438124663f9374 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Mon, 26 Nov 2018 14:29:53 -0500 Subject: [PATCH 195/210] FIX: Use rint instead of ceil --- nistats/hemodynamic_models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nistats/hemodynamic_models.py b/nistats/hemodynamic_models.py index bb82c385..b51a84c1 100644 --- a/nistats/hemodynamic_models.py +++ b/nistats/hemodynamic_models.py @@ -52,7 +52,7 @@ def _gamma_difference_hrf(tr, oversampling=50, time_length=32., onset=0., hrf sampling on the oversampled time grid """ dt = tr / oversampling - time_stamps = np.linspace(0, time_length, np.ceil(float(time_length) / dt).astype(np.int)) + time_stamps = np.linspace(0, time_length, np.rint(float(time_length) / dt).astype(np.int)) time_stamps -= onset hrf = gamma.pdf(time_stamps, delay / dispersion, dt / dispersion) -\ ratio * gamma.pdf( @@ -266,7 +266,7 @@ def _sample_condition(exp_condition, frame_times, oversampling=50, hr_frame_times = np.linspace(frame_times.min() + min_onset, frame_times.max() * (1 + 1. / (n - 1)), - np.ceil(n_hr).astype(np.int)) + np.rint(n_hr).astype(np.int)) # Get the condition information onsets, durations, values = tuple(map(np.asanyarray, exp_condition)) From d1c6ee0611f2943cdfa045ca898415ecf6ff1448 Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Tue, 27 Nov 2018 22:34:59 +0100 Subject: [PATCH 196/210] Improved: CircleCI builds are more efficient - master branch and new branches are always built from scratch. - Existing branches cache their built documentation & reuse that to only rebuild any differences. - The cahce is purged daily, hence the first push of the day is rebuilt from scratch. --- .circleci/config.yml | 109 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 100 insertions(+), 9 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 253b648a..843397a0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,7 +1,12 @@ +# quick-build rebuilds changes using the cached documentation. +# The cache is emptied everyday, forcing a full build on the day's first push. +# It doesn't operate on master branch. New branches are always built from scratch. +# full-build always rebuilds from scratch, without any cache. Only for changes in master branch. + version: 2 jobs: - build: + quick-build: docker: - image: circleci/python:3.6 environment: @@ -23,18 +28,35 @@ jobs: # Installing required packages for `make -C doc check command` to work. - run: sudo -E apt-get -yq update - run: sudo -E apt-get -yq --no-install-suggests --no-install-recommends --force-yes install dvipng texlive-latex-base texlive-latex-extra + - run: + name: Generating today's date and writing to file to generate daily new cache key + command: | + date +%F > today - restore_cache: - key: v1-packages+datasets-{{ .Branch }} - - run: wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O ~/miniconda.sh - - run: chmod +x ~/miniconda.sh && ~/miniconda.sh -b + key: v5-packages+datasets-{{ .Branch }}-{{ checksum "today" }} + - run: + name: Download & install conda if absent + command: | + if + ls $HOME/miniconda3/bin | grep conda -q + then + echo "(Mini)Conda already present from the cache." + else + wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O ~/miniconda.sh + chmod +x ~/miniconda.sh && ~/miniconda.sh -b + fi - run: name: Setup conda path in env variables command: | echo 'export PATH="$HOME/miniconda3/bin:$PATH"' >> $BASH_ENV - run: - name: Create conda env + name: Create new conda env # Separate command so if it fails (when using cache) the entire build will not. command: | - conda create -n testenv python=3.6 numpy scipy scikit-learn matplotlib pandas \ + if conda env list | grep testenv ; then echo "Conda env testenv already exists courtesy of the cache."; else conda create -n testenv -yq; fi + - run: + name: Install packages in conda env + command: | + conda install -n testenv python=3.6 numpy scipy scikit-learn matplotlib pandas \ flake8 lxml nose cython mkl sphinx coverage patsy boto3 pillow pandas -yq conda install -n testenv nibabel nilearn nose-timer -c conda-forge -yq - run: @@ -45,10 +67,11 @@ jobs: set -o pipefail && cd doc && make html 2>&1 | tee ~/log.txt no_output_timeout: 5h - save_cache: - key: v1-packages+datasets-{{ .Branch }} + key: v5-packages+datasets-{{ .Branch }}-{{ checksum "today" }} paths: - - $HOME/nilearn_data - - $HOME/miniconda3 + - ../nilearn_data + - ../miniconda3 + - doc - store_artifacts: path: doc/_build/html @@ -57,3 +80,71 @@ jobs: - store_artifacts: path: $HOME/log.txt destination: log.txt + + + + full-build: + docker: + - image: circleci/python:3.6 + environment: + DISTRIB: "conda" + PYTHON_VERSION: "3.6" + NUMPY_VERSION: "*" + SCIPY_VERSION: "*" + SCIKIT_LEARN_VERSION: "*" + MATPLOTLIB_VERSION: "*" + + steps: + - checkout + # Get rid of existing virtualenvs on circle ci as they conflict with conda. + # Trick found here: + # https://discuss.circleci.com/t/disable-autodetection-of-project-or-application-of-python-venv/235/10 + - run: cd && rm -rf ~/.pyenv && rm -rf ~/virtualenvs + # We need to remove conflicting texlive packages. + - run: sudo -E apt-get -yq remove texlive-binaries --purge + # Installing required packages for `make -C doc check command` to work. + - run: sudo -E apt-get -yq update + - run: sudo -E apt-get -yq --no-install-suggests --no-install-recommends --force-yes install dvipng texlive-latex-base texlive-latex-extra + - run: wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O ~/miniconda.sh + - run: chmod +x ~/miniconda.sh && ~/miniconda.sh -b + - run: echo 'export PATH="$HOME/miniconda3/bin:$PATH"' >> $BASH_ENV + - run: + name: Create conda env + command: | + conda create -n testenv python=3.6 numpy scipy scikit-learn matplotlib pandas \ + flake8 lxml nose cython mkl sphinx coverage patsy boto3 pillow pandas -yq + conda install -n testenv nibabel nilearn nose-timer -c conda-forge -yq + - run: + name: Running CircleCI test (make html) + command: | + source activate testenv + pip install -e . + set -o pipefail && cd doc && make html 2>&1 | tee ~/log.txt + no_output_timeout: 5h + + - store_artifacts: + path: doc/_build/html + - store_artifacts: + path: coverage + - store_artifacts: + path: $HOME/log.txt + destination: log.txt + + +workflows: + version: 2 + push: + jobs: + - quick-build: + filters: + branches: + ignore: + - master + - test-circleci + + - full-build: + filters: + branches: + only: + - master + - test-circleci From fc8ae5360446a57ebd1e26900ecb160a341ac731 Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Tue, 27 Nov 2018 23:13:21 +0100 Subject: [PATCH 197/210] Added a comment to mkae the disappeared in the PR circleci reappear --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 843397a0..f58d36fe 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -140,7 +140,7 @@ workflows: branches: ignore: - master - - test-circleci + - test-circleci # test branch to check if merges occur on master as expected. - full-build: filters: From 9a91a2cbb952acfd4faf176ec3653d9d5fb9e841 Mon Sep 17 00:00:00 2001 From: "Kshitij Chawla (kchawla-pi)" Date: Wed, 28 Nov 2018 16:34:49 +0100 Subject: [PATCH 198/210] Replaced `ols` in comments with `Ordinary Least Squares` in 3 files for clarity --- nistats/second_level_model.py | 2 +- nistats/tests/test_first_level_model.py | 8 ++++---- nistats/tests/test_second_level_model.py | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/nistats/second_level_model.py b/nistats/second_level_model.py index 5c5b6958..a17b471b 100644 --- a/nistats/second_level_model.py +++ b/nistats/second_level_model.py @@ -407,7 +407,7 @@ def compute_contrast( '%i rows in design matrix do not match with %i maps' % (self.design_matrix_.shape[0], len(effect_maps))) - # Fit an OLS regression for parametric statistics + # Fit an Ordinary Least Squares regression for parametric statistics Y = self.masker_.transform(effect_maps) if self.memory: mem_glm = self.memory.cache(run_glm, ignore=['n_jobs']) diff --git a/nistats/tests/test_first_level_model.py b/nistats/tests/test_first_level_model.py index d3e7ff7f..fca916f3 100644 --- a/nistats/tests/test_first_level_model.py +++ b/nistats/tests/test_first_level_model.py @@ -142,7 +142,7 @@ def test_run_glm(): n, p, q = 100, 80, 10 X, Y = np.random.randn(p, q), np.random.randn(p, n) - # ols case + # Ordinary Least Squares case labels, results = run_glm(Y, X, 'ols') assert_array_equal(labels, np.zeros(n)) assert_equal(list(results.keys()), [0.0]) @@ -262,7 +262,7 @@ def test_first_level_model_glm_computation(): t_r = 10.0 slice_time_ref = 0. events = basic_paradigm() - # ols case + # Ordinary Least Squares case model = FirstLevelModel(t_r, slice_time_ref, mask=mask, drift_model='polynomial', drift_order=3, minimize_memory=False) @@ -287,7 +287,7 @@ def test_first_level_glm_computation_with_memory_caching(): t_r = 10.0 slice_time_ref = 0. events = basic_paradigm() - # ols case + # Ordinary Least Squares case model = FirstLevelModel(t_r, slice_time_ref, mask=mask, drift_model='polynomial', drift_order=3, memory='nilearn_cache', memory_level=1, @@ -305,7 +305,7 @@ def test_first_level_model_contrast_computation(): t_r = 10.0 slice_time_ref = 0. events = basic_paradigm() - # ols case + # Ordinary Least Squares case model = FirstLevelModel(t_r, slice_time_ref, mask=mask, drift_model='polynomial', drift_order=3, minimize_memory=False) diff --git a/nistats/tests/test_second_level_model.py b/nistats/tests/test_second_level_model.py index cbd8f1b1..ad02ded4 100644 --- a/nistats/tests/test_second_level_model.py +++ b/nistats/tests/test_second_level_model.py @@ -46,7 +46,7 @@ def test_high_level_glm_with_paths(): mask, FUNCFILE, _ = write_fake_fmri_data(shapes) FUNCFILE = FUNCFILE[0] func_img = load(FUNCFILE) - # ols case + # Ordinary Least Squares case model = SecondLevelModel(mask=mask) # asking for contrast before model fit gives error assert_raises(ValueError, model.compute_contrast, []) @@ -150,7 +150,7 @@ def test_second_level_model_glm_computation(): mask, FUNCFILE, _ = write_fake_fmri_data(shapes) FUNCFILE = FUNCFILE[0] func_img = load(FUNCFILE) - # ols case + # Ordinary Least Squares case model = SecondLevelModel(mask=mask) Y = [func_img] * 4 X = pd.DataFrame([[1]] * 4, columns=['intercept']) @@ -172,7 +172,7 @@ def test_second_level_model_contrast_computation(): mask, FUNCFILE, _ = write_fake_fmri_data(shapes) FUNCFILE = FUNCFILE[0] func_img = load(FUNCFILE) - # ols case + # Ordinary Least Squares case model = SecondLevelModel(mask=mask) # asking for contrast before model fit gives error assert_raises(ValueError, model.compute_contrast, 'intercept') @@ -213,7 +213,7 @@ def test_second_level_model_contrast_computation_with_memory_caching(): mask, FUNCFILE, _ = write_fake_fmri_data(shapes) FUNCFILE = FUNCFILE[0] func_img = load(FUNCFILE) - # ols case + # Ordinary Least Squares case model = SecondLevelModel(mask=mask, memory='nilearn_cache') # fit model Y = [func_img] * 4 From e9702408d05b7a484546357f45e8f976abf155c2 Mon Sep 17 00:00:00 2001 From: "Kshitij Chawla (kchawla-pi)" Date: Wed, 28 Nov 2018 16:39:55 +0100 Subject: [PATCH 199/210] Spurious commit to trigger the disappeared TravisCI --- nistats/second_level_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nistats/second_level_model.py b/nistats/second_level_model.py index a17b471b..5a813483 100644 --- a/nistats/second_level_model.py +++ b/nistats/second_level_model.py @@ -400,7 +400,7 @@ def compute_contrast( # Get effect_maps appropriate for chosen contrast effect_maps = _infer_effect_maps(self.second_level_input_, first_level_contrast) - # check design matrix X and effect maps Y agree on number of rows + # Check design matrix X and effect maps Y agree on number of rows if len(effect_maps) != self.design_matrix_.shape[0]: raise ValueError( 'design_matrix does not match the number of maps considered. ' From 976e93946ad91cf453dd711c3249afd31be0fb81 Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Thu, 29 Nov 2018 11:25:04 +0100 Subject: [PATCH 200/210] Fixed: param img_filters no longer has a mutable arg ([]) as default --- nistats/first_level_model.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/nistats/first_level_model.py b/nistats/first_level_model.py index bb92a5b0..16a7257d 100644 --- a/nistats/first_level_model.py +++ b/nistats/first_level_model.py @@ -567,7 +567,7 @@ def compute_contrast(self, contrast_def, stat_type=None, def first_level_models_from_bids( - dataset_path, task_label, space_label, img_filters=[], + dataset_path, task_label, space_label, img_filters=None, t_r=None, slice_time_ref=0., hrf_model='glover', drift_model='cosine', period_cut=128, drift_order=1, fir_delays=[0], min_onset=-24, mask=None, target_affine=None, target_shape=None, smoothing_fwhm=None, @@ -593,7 +593,7 @@ def first_level_models_from_bids( Specifies the space label of the preproc.nii images. As they are specified in the file names like _space-_. - img_filters: list of tuples (str, str), optional (default: []) + img_filters: list of tuples (str, str), optional (default: None) Filters are of the form (field, label). Only one filter per field allowed. A file that does not match a filter will be discarded. Possible filters are 'acq', 'rec', 'run', 'res' and 'variant'. @@ -625,6 +625,7 @@ def first_level_models_from_bids( Items for the FirstLevelModel fit function of their respective model. """ # check arguments + img_filters = img_filters if img_filters else [] if not isinstance(dataset_path, str): raise TypeError('dataset_path must be a string, instead %s was given' % type(task_label)) From b3d90589d87071c93eb8774c1729e471e1ef9515 Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Fri, 30 Nov 2018 11:52:20 +0100 Subject: [PATCH 201/210] Reformatted commands for clarity after discussion with Gael --- .circleci/config.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 17d5647d..4f57d8de 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -50,9 +50,15 @@ jobs: command: | echo 'export PATH="$HOME/miniconda3/bin:$PATH"' >> $BASH_ENV - run: - name: Create new conda env # Separate command so if it fails (when using cache) the entire build will not. + name: Create new conda env command: | - if conda env list | grep testenv ; then echo "Conda env testenv already exists courtesy of the cache."; else conda create -n testenv -yq; fi + if + conda env list | grep testenv + then + echo "Conda env testenv already exists courtesy of the cache." + else + conda create -n testenv -yq + fi - run: name: Install packages in conda env command: | From db497e8f7fbecbd17971474b3f463fc4a5231cd5 Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Sun, 2 Dec 2018 13:40:21 +0100 Subject: [PATCH 202/210] Added mechanism to generate new cache by external contributors --- .circleci/cache-timestamp.txt | 1 + .circleci/clean-cache.sh | 3 +++ .circleci/config.yml | 11 +++++++++-- 3 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 .circleci/cache-timestamp.txt create mode 100755 .circleci/clean-cache.sh diff --git a/.circleci/cache-timestamp.txt b/.circleci/cache-timestamp.txt new file mode 100644 index 00000000..7e055823 --- /dev/null +++ b/.circleci/cache-timestamp.txt @@ -0,0 +1 @@ +dimanche 2 décembre 2018, 13:37:13 (UTC+0100) diff --git a/.circleci/clean-cache.sh b/.circleci/clean-cache.sh new file mode 100755 index 00000000..b5950e86 --- /dev/null +++ b/.circleci/clean-cache.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +date > cache-timestamp.txt diff --git a/.circleci/config.yml b/.circleci/config.yml index 4f57d8de..fcd8beff 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -33,7 +33,7 @@ jobs: command: | date +%F > today - restore_cache: - key: v5-packages+datasets-{{ .Branch }}-{{ checksum "today" }} + key: v5-packages+datasets-{{ .Branch }}-{{ checksum "today" }}-{{ checksum ".circleci/cache-timestamp.txt" }} - run: name: Download & install conda if absent command: | @@ -73,7 +73,7 @@ jobs: set -o pipefail && cd doc && make html-strict 2>&1 | tee ~/log.txt no_output_timeout: 5h - save_cache: - key: v5-packages+datasets-{{ .Branch }}-{{ checksum "today" }} + key: v5-packages+datasets-{{ .Branch }}-{{ checksum "today" }}-{{ checksum ".circleci/cache-timestamp.txt" }} paths: - ../nilearn_data - ../miniconda3 @@ -128,6 +128,13 @@ jobs: set -o pipefail && cd doc && make html 2>&1 | tee ~/log.txt no_output_timeout: 5h + - save_cache: + key: v5-packages+datasets-{{ .Branch }}-{{ checksum "today" }}-{{ checksum ".circleci/cache-timestamp.txt" }} + paths: + - ../nilearn_data + - ../miniconda3 + - doc + - store_artifacts: path: doc/_build/html - store_artifacts: From e5f70ccf0d1cc091633540b558e5a649101f1087 Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Sun, 2 Dec 2018 15:04:08 +0100 Subject: [PATCH 203/210] Renamed cache timestamp's filename; clarify its only for manual triggered rebuilds --- .circleci/clean-cache.sh | 2 +- .circleci/config.yml | 6 +++--- .../{cache-timestamp.txt => manual-cache-timestamp.txt} | 0 3 files changed, 4 insertions(+), 4 deletions(-) rename .circleci/{cache-timestamp.txt => manual-cache-timestamp.txt} (100%) diff --git a/.circleci/clean-cache.sh b/.circleci/clean-cache.sh index b5950e86..fae4fc28 100755 --- a/.circleci/clean-cache.sh +++ b/.circleci/clean-cache.sh @@ -1,3 +1,3 @@ #!/usr/bin/env bash -date > cache-timestamp.txt +date > manual-cache-timestamp.txt diff --git a/.circleci/config.yml b/.circleci/config.yml index fcd8beff..6c07c01b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -33,7 +33,7 @@ jobs: command: | date +%F > today - restore_cache: - key: v5-packages+datasets-{{ .Branch }}-{{ checksum "today" }}-{{ checksum ".circleci/cache-timestamp.txt" }} + key: v5-packages+datasets-{{ .Branch }}-{{ checksum "today" }}-{{ checksum ".circleci/manual-cache-timestamp.txt" }} - run: name: Download & install conda if absent command: | @@ -73,7 +73,7 @@ jobs: set -o pipefail && cd doc && make html-strict 2>&1 | tee ~/log.txt no_output_timeout: 5h - save_cache: - key: v5-packages+datasets-{{ .Branch }}-{{ checksum "today" }}-{{ checksum ".circleci/cache-timestamp.txt" }} + key: v5-packages+datasets-{{ .Branch }}-{{ checksum "today" }}-{{ checksum ".circleci/manual-cache-timestamp.txt" }} paths: - ../nilearn_data - ../miniconda3 @@ -129,7 +129,7 @@ jobs: no_output_timeout: 5h - save_cache: - key: v5-packages+datasets-{{ .Branch }}-{{ checksum "today" }}-{{ checksum ".circleci/cache-timestamp.txt" }} + key: v5-packages+datasets-{{ .Branch }}-{{ checksum "today" }}-{{ checksum ".circleci/manual-cache-timestamp.txt" }} paths: - ../nilearn_data - ../miniconda3 diff --git a/.circleci/cache-timestamp.txt b/.circleci/manual-cache-timestamp.txt similarity index 100% rename from .circleci/cache-timestamp.txt rename to .circleci/manual-cache-timestamp.txt From 855953113636081f8adc2cad2041ef13a4a6a283 Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Sun, 2 Dec 2018 15:08:46 +0100 Subject: [PATCH 204/210] Changed date format to be internation for manual cache rebuild trigger --- .circleci/clean-cache.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/clean-cache.sh b/.circleci/clean-cache.sh index fae4fc28..0f850bb7 100755 --- a/.circleci/clean-cache.sh +++ b/.circleci/clean-cache.sh @@ -1,3 +1,3 @@ #!/usr/bin/env bash -date > manual-cache-timestamp.txt +date +%F+%T+%:::z > manual-cache-timestamp.txt From afd971da057202f09f18da82fbc0a6ed0370a307 Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Sun, 2 Dec 2018 15:13:49 +0100 Subject: [PATCH 205/210] Manually triggering a fresh cache build for testing --- .circleci/manual-cache-timestamp.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/manual-cache-timestamp.txt b/.circleci/manual-cache-timestamp.txt index 7e055823..224dd25e 100644 --- a/.circleci/manual-cache-timestamp.txt +++ b/.circleci/manual-cache-timestamp.txt @@ -1 +1 @@ -dimanche 2 décembre 2018, 13:37:13 (UTC+0100) +2018-12-02+15:13:14++01 From 1c2baf0fecaa79091d59bf179161142048539677 Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Sun, 2 Dec 2018 15:52:08 +0100 Subject: [PATCH 206/210] Removed the provision for caching post `master` merge since it is pointless. --- .circleci/config.yml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 6c07c01b..5bf9918d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -128,13 +128,6 @@ jobs: set -o pipefail && cd doc && make html 2>&1 | tee ~/log.txt no_output_timeout: 5h - - save_cache: - key: v5-packages+datasets-{{ .Branch }}-{{ checksum "today" }}-{{ checksum ".circleci/manual-cache-timestamp.txt" }} - paths: - - ../nilearn_data - - ../miniconda3 - - doc - - store_artifacts: path: doc/_build/html - store_artifacts: From 87c1011ad25cb57b83003ffa49f831c2628d41a0 Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Mon, 3 Dec 2018 09:57:14 +0100 Subject: [PATCH 207/210] Replaced clean-cache.sh with clean-cache.py (more reliable path determination) - Renamed manual-cache-timestamp.txt to manual-cache-timestamp --- .circleci/clean-cache.py | 13 +++++++++++++ .circleci/clean-cache.sh | 3 --- .circleci/config.yml | 4 ++-- ...l-cache-timestamp.txt => manual-cache-timestamp} | 0 4 files changed, 15 insertions(+), 5 deletions(-) create mode 100755 .circleci/clean-cache.py delete mode 100755 .circleci/clean-cache.sh rename .circleci/{manual-cache-timestamp.txt => manual-cache-timestamp} (100%) diff --git a/.circleci/clean-cache.py b/.circleci/clean-cache.py new file mode 100755 index 00000000..7a9d0d03 --- /dev/null +++ b/.circleci/clean-cache.py @@ -0,0 +1,13 @@ +#! /usr/bin/env python + +import os +from datetime import datetime as dt + + +'nilearn/.circleci' in __file__ +timestamp_dirpath = os.path.dirname(__file__) +timestamp_filename = 'manual-cache-timestamp' +timestamp_filepath = os.path.join(timestamp_dirpath, timestamp_filename) +utc_now_timestamp = dt.utcnow() +with open(timestamp_filepath, 'w') as write_obj: + write_obj.write(str(utc_now_timestamp)) diff --git a/.circleci/clean-cache.sh b/.circleci/clean-cache.sh deleted file mode 100755 index 0f850bb7..00000000 --- a/.circleci/clean-cache.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env bash - -date +%F+%T+%:::z > manual-cache-timestamp.txt diff --git a/.circleci/config.yml b/.circleci/config.yml index 5bf9918d..ba12159d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -33,7 +33,7 @@ jobs: command: | date +%F > today - restore_cache: - key: v5-packages+datasets-{{ .Branch }}-{{ checksum "today" }}-{{ checksum ".circleci/manual-cache-timestamp.txt" }} + key: v5-packages+datasets-{{ .Branch }}-{{ checksum "today" }}-{{ checksum ".circleci/manual-cache-timestamp" }} - run: name: Download & install conda if absent command: | @@ -73,7 +73,7 @@ jobs: set -o pipefail && cd doc && make html-strict 2>&1 | tee ~/log.txt no_output_timeout: 5h - save_cache: - key: v5-packages+datasets-{{ .Branch }}-{{ checksum "today" }}-{{ checksum ".circleci/manual-cache-timestamp.txt" }} + key: v5-packages+datasets-{{ .Branch }}-{{ checksum "today" }}-{{ checksum ".circleci/manual-cache-timestamp" }} paths: - ../nilearn_data - ../miniconda3 diff --git a/.circleci/manual-cache-timestamp.txt b/.circleci/manual-cache-timestamp similarity index 100% rename from .circleci/manual-cache-timestamp.txt rename to .circleci/manual-cache-timestamp From a86e5f3af13696c8084e93c995624abf50a9fc07 Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Mon, 3 Dec 2018 13:29:40 +0100 Subject: [PATCH 208/210] Cleaned up some python code, fixed the incorrect circleci log upload --- .circleci/clean-cache.py | 17 ++++++++++------- .circleci/config.yml | 4 +--- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/.circleci/clean-cache.py b/.circleci/clean-cache.py index 7a9d0d03..b6fd596d 100755 --- a/.circleci/clean-cache.py +++ b/.circleci/clean-cache.py @@ -3,11 +3,14 @@ import os from datetime import datetime as dt +def update_cache_timestamp(): + timestamp_dirpath = os.path.dirname(__file__) + timestamp_filename = 'manual-cache-timestamp' + timestamp_filepath = os.path.join(timestamp_dirpath, timestamp_filename) + utc_now_timestamp = dt.utcnow() + with open(timestamp_filepath, 'w') as write_obj: + write_obj.write(str(utc_now_timestamp)) -'nilearn/.circleci' in __file__ -timestamp_dirpath = os.path.dirname(__file__) -timestamp_filename = 'manual-cache-timestamp' -timestamp_filepath = os.path.join(timestamp_dirpath, timestamp_filename) -utc_now_timestamp = dt.utcnow() -with open(timestamp_filepath, 'w') as write_obj: - write_obj.write(str(utc_now_timestamp)) + +if __name__ == '__main__': + update_cache_timestamp() diff --git a/.circleci/config.yml b/.circleci/config.yml index ba12159d..7ca45148 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -84,9 +84,7 @@ jobs: - store_artifacts: path: coverage - store_artifacts: - path: $HOME/log.txt - destination: log.txt - + path: log.txt full-build: From ca9e2be6f19ee1cc1f71f04e2e819994196349f6 Mon Sep 17 00:00:00 2001 From: Kshitij Chawla Date: Tue, 4 Dec 2018 16:26:17 +0100 Subject: [PATCH 209/210] Added a README.md in .circleci to explain to users how force cache rebuilding --- .circleci/README.md | 14 ++++++++++++++ .circleci/clean-cache.py | 7 +++++++ 2 files changed, 21 insertions(+) create mode 100644 .circleci/README.md diff --git a/.circleci/README.md b/.circleci/README.md new file mode 100644 index 00000000..cbf18d62 --- /dev/null +++ b/.circleci/README.md @@ -0,0 +1,14 @@ +CircleCI is used to build our documentations and tutorial examples. +CircleCI's cache stores previously built documentation and +only rebuild any changes, instead of rebuilding the documentation from scratch. +This saves a lot of time. + +Occasionally, some changes necessitate rebuilding the documentation from scratch, +either to see the full effect of the changes +or because the cached builds are raising some error. + +To run a new CircleCI build from the beginning, without using the cache: + +1. Run the script `clean-cache.py`. +2. Commit the change (with a clear message). +3. Push the commit. diff --git a/.circleci/clean-cache.py b/.circleci/clean-cache.py index b6fd596d..60c86a20 100755 --- a/.circleci/clean-cache.py +++ b/.circleci/clean-cache.py @@ -4,6 +4,13 @@ from datetime import datetime as dt def update_cache_timestamp(): + """ Updates the contents of the manual-cache-timestamp file + with current timestamp. + + Returns + ------- + None + """ timestamp_dirpath = os.path.dirname(__file__) timestamp_filename = 'manual-cache-timestamp' timestamp_filepath = os.path.join(timestamp_dirpath, timestamp_filename) From 985f4d5b238e244048446cc9877981e2f25a8425 Mon Sep 17 00:00:00 2001 From: Gilles de Hollander Date: Fri, 7 Dec 2018 16:00:36 +0100 Subject: [PATCH 210/210] SimpleRegressionResults can still give residuals and predictions when X or Y is provided --- nistats/regression.py | 15 ++++++++------- nistats/tests/test_regression.py | 12 +++++++++++- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/nistats/regression.py b/nistats/regression.py index 6ae048b8..c7d928a3 100644 --- a/nistats/regression.py +++ b/nistats/regression.py @@ -357,15 +357,17 @@ def logL(self, Y): """ The maximized log-likelihood """ - raise ValueError('can not use this method for simple results') + raise NotImplementedError('logL not implemented for ' + 'SimpleRegressionsResults. ' + 'Use RegressionResults') - def resid(self, Y): + def resid(self, Y, X): """ Residuals from the fit. """ - return Y - self.predicted + return Y - self.predicted(X) - def norm_resid(self, Y): + def norm_resid(self, Y, X): """ Residuals, normalized to have unit length. @@ -382,12 +384,11 @@ def norm_resid(self, Y): See: Montgomery and Peck 3.2.1 p. 68 Davidson and MacKinnon 15.2 p 662 """ - return self.resid(Y) * positive_reciprocal(np.sqrt(self.dispersion)) + return self.resid(Y, X) * positive_reciprocal(np.sqrt(self.dispersion)) - def predicted(self): + def predicted(self, X): """ Return linear predictor values from a design matrix. """ beta = self.theta # the LikelihoodModelResults has parameters named 'theta' - X = self.model.design return np.dot(X, beta) diff --git a/nistats/tests/test_regression.py b/nistats/tests/test_regression.py index 45842da2..de810615 100644 --- a/nistats/tests/test_regression.py +++ b/nistats/tests/test_regression.py @@ -4,7 +4,7 @@ import numpy as np -from nistats.regression import OLSModel, ARModel +from nistats.regression import OLSModel, ARModel, SimpleRegressionResults from nose.tools import assert_equal, assert_true from numpy.testing import assert_array_almost_equal, assert_array_equal @@ -41,3 +41,13 @@ def test_AR_degenerate(): model = ARModel(design=Xd, rho=0.9) results = model.fit(Y) assert_equal(results.df_resid, 31) + +def test_simple_results(): + model = OLSModel(X) + results = model.fit(Y) + residfull = results.resid + predictedfull = results.predicted + + simple_results = SimpleRegressionResults(results) + assert_array_equal(results.predicted, simple_results.predicted(X)) + assert_array_equal(results.resid, simple_results.resid(Y, X))