Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[MAINT] refactor of tests and code in GLM module #4066

Merged
merged 19 commits into from
Oct 24, 2023
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion doc/changes/latest.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Enhancements
- :bdg-primary:`Doc` Add backslash to homogenize :class:`~nilearn.regions.Parcellations` documentation (:gh:`4042` by `Nikhil Krish`_).
- :bdg-success:`API` Allow passing Pandas Series of image filenames to :class:`~nilearn.glm.second_level.SecondLevelModel` (:gh:`4070` by `Rémi Gau`_).
- Allow setting ``vmin`` in :func:`~nilearn.plotting.plot_glass_brain` and :func:`~nilearn.plotting.plot_stat_map` (:gh:`3993` by `Michelle Wang`_).
- :bdg-success:`API` Support passing t and F contrasts to :func:`~nilearn.glm.contrasts.compute_contrast` that that have fewer columns than the number of estimated parameters. Remaining columns are padded with zero (:gh:`4067` by `Remi Gau`_).
- :bdg-success:`API` Support passing t and F contrasts to :func:`~nilearn.glm.contrasts.compute_contrast` that that have fewer columns than the number of estimated parameters. Remaining columns are padded with zero (:gh:`4067` by `Rémi Gau`_).
Remi-Gau marked this conversation as resolved.
Show resolved Hide resolved

Changes
-------
17 changes: 6 additions & 11 deletions nilearn/glm/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,8 @@ def _check_and_load_tables(tables_, var_name):
pass
else:
raise TypeError(
"%s can only be a pandas DataFrames or a"
"string. A %s was provided at idx %d"
% (var_name, type(table), table_idx)
f"{var_name} can only be a pandas DataFrames or a string. "
f"A {type(table)} was provided at idx {table_idx}"
)
return tables

Expand Down Expand Up @@ -109,6 +108,8 @@ def _check_events_file_uses_tab_separators(events_files):
if not isinstance(events_files, (list, tuple)):
events_files = [events_files]
for events_file_ in events_files:
if isinstance(events_file_, (pd.DataFrame)):
continue
try:
with open(events_file_) as events_file_obj:
events_file_sample = events_file_obj.readline()
Expand All @@ -118,12 +119,6 @@ def _check_events_file_uses_tab_separators(events_files):
Handling them here will beak the calling code,
and refactoring that is not straightforward.
"""
except TypeError: # events is Pandas dataframe.
pass
except UnicodeDecodeError: # py3:if binary file
raise ValueError(
"The file does not seem to be a valid unicode text file."
)
except OSError: # if invalid filepath.
pass
else:
Expand All @@ -132,13 +127,13 @@ def _check_events_file_uses_tab_separators(events_files):
sample=events_file_sample,
delimiters=valid_separators,
)
except csv.Error:
except csv.Error as e:
raise ValueError(
"The values in the events file "
"are not separated by tabs; "
"please enforce BIDS conventions",
events_file_,
)
) from e


def _check_run_tables(run_imgs, tables_, tables_name):
Expand Down
22 changes: 8 additions & 14 deletions nilearn/glm/contrasts.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,16 +145,13 @@ def _compute_fixed_effect_contrast(
n_contrasts = 0
for i, (lab, res, con_val) in enumerate(zip(labels, results, con_vals)):
if np.all(con_val == 0):
warn(f"Contrast for session {int(i)} is null")
warn(f"Contrast for session {int(i)} is null.")
continue
contrast_ = compute_contrast(lab, res, con_val, contrast_type)
if contrast is None:
contrast = contrast_
else:
contrast = contrast + contrast_
contrast = contrast_ if contrast is None else contrast + contrast_
n_contrasts += 1
if contrast is None:
raise ValueError("all contrasts provided were null contrasts")
raise ValueError("All contrasts provided were null contrasts.")
return contrast * (1.0 / n_contrasts)


Expand Down Expand Up @@ -216,10 +213,7 @@ def __init__(
self.effect = effect
self.variance = variance
self.dof = float(dof)
if dim is None:
self.dim = effect.shape[0]
else:
self.dim = dim
self.dim = effect.shape[0] if dim is None else dim
if self.dim > 1 and contrast_type == "t":
print("Automatically converted multi-dimensional t to F contrast")
contrast_type = "F"
Expand Down Expand Up @@ -297,7 +291,7 @@ def p_value(self, baseline=0.0):
p-values, one per voxel

"""
if self.stat_ is None or not self.baseline == baseline:
if self.stat_ is None or self.baseline != baseline:
self.stat_ = self.stat(baseline)
# Valid conjunction as in Nichols et al, Neuroimage 25, 2005.
if self.contrast_type == "t":
Expand Down Expand Up @@ -329,7 +323,7 @@ def one_minus_pvalue(self, baseline=0.0):
one_minus_pvalues, one per voxel

"""
if self.stat_ is None or not self.baseline == baseline:
if self.stat_ is None or self.baseline != baseline:
self.stat_ = self.stat(baseline)
# Valid conjunction as in Nichols et al, Neuroimage 25, 2005.
if self.contrast_type == "t":
Expand Down Expand Up @@ -360,7 +354,7 @@ def z_score(self, baseline=0.0):
statistical values, one per voxel

"""
if self.p_value_ is None or not self.baseline == baseline:
if self.p_value_ is None or self.baseline != baseline:
self.p_value_ = self.p_value(baseline)
if self.one_minus_pvalue_ is None:
self.one_minus_pvalue_ = self.one_minus_pvalue(baseline)
Expand Down Expand Up @@ -473,7 +467,7 @@ def compute_fixed_effects(
if n_runs != len(variance_imgs):
raise ValueError(
f"The number of contrast images ({len(contrast_imgs)}) differs "
f"from the number of variance images ({len(variance_imgs)}). "
f"from the number of variance images ({len(variance_imgs)})."
)

if isinstance(mask, NiftiMasker):
Expand Down
2 changes: 1 addition & 1 deletion nilearn/glm/first_level/design_matrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -442,7 +442,7 @@ def check_design_matrix(design_matrix):
Per-event onset time (in seconds)

"""
names = [name for name in design_matrix.keys()]
names = list(design_matrix.keys())
frame_times = design_matrix.index
matrix = design_matrix.values
return frame_times, matrix, names
Expand Down
17 changes: 7 additions & 10 deletions nilearn/glm/first_level/first_level.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ def run_glm(
acceptable_noise_models = ["ols", "arN"]
if (noise_model[:2] != "ar") and (noise_model != "ols"):
raise ValueError(
f"Acceptable noise models are {acceptable_noise_models}."
f"Acceptable noise models are {acceptable_noise_models}. "
f"You provided 'noise_model={noise_model}'."
)
if Y.shape[0] != X.shape[0]:
Expand Down Expand Up @@ -439,10 +439,7 @@ def __init__(
self.target_shape = target_shape
self.smoothing_fwhm = smoothing_fwhm
memory = stringify_path(memory)
if isinstance(memory, str):
self.memory = Memory(memory)
else:
self.memory = memory
self.memory = Memory(memory) if isinstance(memory, str) else memory
self.memory_level = memory_level
self.standardize = standardize
if signal_scaling is False:
Expand Down Expand Up @@ -652,7 +649,7 @@ def fit(
confounds_matrix = confounds[run_idx].values
if confounds_matrix.shape[0] != n_scans:
raise ValueError(
"Rows in confounds does not match"
"Rows in confounds does not match "
"n_scans in run_img "
f"at index {run_idx}."
)
Expand Down Expand Up @@ -779,7 +776,7 @@ def compute_contrast(

"""
if self.labels_ is None or self.results_ is None:
raise ValueError("The model has not been fit yet")
raise ValueError("The model has not been fit yet.")

if isinstance(contrast_def, (np.ndarray, str)):
con_vals = [contrast_def]
Expand All @@ -788,13 +785,13 @@ def compute_contrast(
else:
raise ValueError(
"contrast_def must be an array or str or list of"
" (array or str)"
" (array or str)."
)

n_runs = len(self.labels_)
n_contrasts = len(con_vals)
if n_contrasts == 1 and n_runs > 1:
warn(f"One contrast given, assuming it for all {int(n_runs)} runs")
warn(f"One contrast given, assuming it for all {n_runs} runs")
con_vals = con_vals * n_runs
elif n_contrasts != n_runs:
raise ValueError(
Expand Down Expand Up @@ -884,7 +881,7 @@ def _get_voxelwise_model_attribute(self, attribute, result_as_time_series):
)

if self.labels_ is None or self.results_ is None:
raise ValueError("The model has not been fit yet")
raise ValueError("The model has not been fit yet.")

output = []

Expand Down
6 changes: 2 additions & 4 deletions nilearn/glm/first_level/hemodynamic_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -542,14 +542,12 @@ def _regressor_names(con_name, hrf_model, fir_delays=None):
names = [con_name, f"{con_name}_derivative", f"{con_name}_dispersion"]
elif hrf_model == "fir":
names = [f"{con_name}_delay_{int(i)}" for i in fir_delays]
# Handle callables
elif callable(hrf_model):
names = [f"{con_name}_{hrf_model.__name__}"]
elif isinstance(hrf_model, Iterable) and all(
[callable(_) for _ in hrf_model]
callable(_) for _ in hrf_model
):
names = [f"{con_name}_{model.__name__}" for model in hrf_model]
# Handle some default cases
elif isinstance(hrf_model, Iterable) and not isinstance(hrf_model, str):
names = [f"{con_name}_{i}" for i in range(len(hrf_model))]

Expand Down Expand Up @@ -640,7 +638,7 @@ def _hrf_kernel(hrf_model, tr, oversampling=50, fir_delays=None):
except TypeError:
raise ValueError(error_msg)
elif isinstance(hrf_model, Iterable) and all(
[callable(_) for _ in hrf_model]
callable(_) for _ in hrf_model
):
try:
hkernel = [model(tr, oversampling) for model in hrf_model]
Expand Down
10 changes: 7 additions & 3 deletions nilearn/glm/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,8 +194,12 @@ def Tcontrast(self, matrix, store=("t", "effect", "sd"), dispersion=None):
# 1D vectors assumed to be row vector
if matrix.ndim == 1:
matrix = matrix[None]
if matrix.size == 0:
raise ValueError("t contrasts cannot be empty: " f"got {matrix}")
if matrix.shape[0] != 1:
raise ValueError("t contrasts should have only one row")
raise ValueError(
"t contrasts should have only one row: " f"got {matrix}."
)
matrix = pad_contrast(
con_val=matrix, theta=self.theta, contrast_type="t"
)
Expand Down Expand Up @@ -263,8 +267,8 @@ def Fcontrast(self, matrix, dispersion=None, invcov=None):
matrix = matrix[None]
if matrix.shape[1] != self.theta.shape[0]:
raise ValueError(
f"F contrasts should have shape[1] P={self.theta.shape[0]}, "
f"but this has shape[1] {matrix.shape[1]}"
f"F contrasts should have shape[1]={self.theta.shape[0]}, "
f"but this has shape[1]={matrix.shape[1]}"
)
matrix = pad_contrast(
con_val=matrix, theta=self.theta, contrast_type="F"
Expand Down
26 changes: 13 additions & 13 deletions nilearn/glm/second_level/second_level.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ def _check_confounds(confounds):
)
# Make sure subject_label contain strings
if not all(
[isinstance(_, str) for _ in confounds["subject_label"].tolist()]
isinstance(_, str) for _ in confounds["subject_label"].tolist()
):
raise ValueError("subject_label column must contain only strings")

Expand All @@ -250,9 +250,10 @@ def _check_output_type(output_type, valid_types):

def _check_design_matrix(design_matrix):
"""Check design_matrix type."""
if design_matrix is not None:
if not isinstance(design_matrix, pd.DataFrame):
raise ValueError("design matrix must be a pandas DataFrame")
if design_matrix is not None and not isinstance(
design_matrix, pd.DataFrame
):
raise ValueError("design matrix must be a pandas DataFrame")


def _check_effect_maps(effect_maps, design_matrix):
Expand Down Expand Up @@ -442,10 +443,7 @@ def __init__(
self.target_shape = target_shape
self.smoothing_fwhm = smoothing_fwhm
memory = stringify_path(memory)
if isinstance(memory, str):
self.memory = Memory(memory)
else:
self.memory = memory
self.memory = Memory(memory) if isinstance(memory, str) else memory
self.memory_level = memory_level
self.verbose = verbose
self.n_jobs = n_jobs
Expand Down Expand Up @@ -592,7 +590,7 @@ def compute_contrast(

"""
if self.second_level_input_ is None:
raise ValueError("The model has not been fit yet")
raise ValueError("The model has not been fit yet.")

# check first_level_contrast
_check_first_level_contrast(
Expand Down Expand Up @@ -955,10 +953,12 @@ def non_parametric_inference(

else:
masker = clone(mask)
if smoothing_fwhm is not None:
if getattr(masker, "smoothing_fwhm") is not None:
warn("Parameter smoothing_fwhm of the masker overridden")
setattr(masker, "smoothing_fwhm", smoothing_fwhm)
if (
smoothing_fwhm is not None
and getattr(masker, "smoothing_fwhm") is not None
):
warn("Parameter smoothing_fwhm of the masker overridden")
setattr(masker, "smoothing_fwhm", smoothing_fwhm)

masker.fit(sample_map)

Expand Down
22 changes: 8 additions & 14 deletions nilearn/glm/tests/test_check_events_file_uses_tab_separators.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ def _create_test_file(temp_csv, test_data, delimiter):

def _run_test_for_invalid_separator(filepath, delimiter_name):
if delimiter_name not in ("tab", "comma"):
with pytest.raises(ValueError):
with pytest.raises(
ValueError,
match="The values in the events file are not separated by tabs",
):
_check_events_file_uses_tab_separators(events_files=filepath)
else:
result = _check_events_file_uses_tab_separators(events_files=filepath)
Expand Down Expand Up @@ -86,22 +89,13 @@ def test_for_pandas_dataframe():
assert result is None


def test_binary_opening_an_image_error(tmp_path):
img_data = bytearray(
b"GIF87a\x01\x00\x01\x00\xe7*\x00\x00\x00\x00\x01\x01\x01\x02\x02"
b"\x07\x08\x08\x08\x0b\x0b\x0b\x0c\x0c\x0c\r;"
)
temp_img_file = tmp_path / "temp_img.gif"
with open(temp_img_file, "wb") as temp_img_obj:
temp_img_obj.write(img_data)
with pytest.raises(ValueError):
_check_events_file_uses_tab_separators(events_files=temp_img_file)


def test_binary_bytearray_of_ints_data_error(tmp_path):
temp_data_bytearray_from_ints = bytearray([0, 1, 0, 11, 10])
temp_bin_file = tmp_path / "temp_bin.bin"
with open(temp_bin_file, "wb") as temp_bin_obj:
temp_bin_obj.write(temp_data_bytearray_from_ints)
with pytest.raises(ValueError):
with pytest.raises(
ValueError,
match="The values in the events file are not separated by tabs",
):
_check_events_file_uses_tab_separators(events_files=temp_bin_file)