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

[ENH] Allow string expressions for second_level_contrast of non_parametric_inference #4150

Merged
merged 6 commits into from
Jan 8, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
43 changes: 12 additions & 31 deletions nilearn/glm/second_level/second_level.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,9 +274,13 @@ def _get_con_val(second_level_contrast, design_matrix):
else:
raise ValueError("No second-level contrast is specified.")
if not isinstance(second_level_contrast, str):
con_val = second_level_contrast
if np.all(con_val == 0):
raise ValueError("Contrast is null")
con_val = np.array(second_level_contrast)
if np.all(con_val == 0) or len(con_val) == 0:
raise ValueError(
"Contrast is null. Second_level_contrast must be a valid "
"contrast vector, a list/array of 0s and 1s, a string, or a "
"string expression."
)
ymzayek marked this conversation as resolved.
Show resolved Hide resolved
else:
design_columns = design_matrix.columns.tolist()
con_val = expression_to_contrast_vector(
Expand All @@ -285,31 +289,6 @@ def _get_con_val(second_level_contrast, design_matrix):
return con_val


def _get_contrast(second_level_contrast, design_matrix):
"""Check and return contrast when testing one contrast at the time."""
if isinstance(second_level_contrast, str):
if second_level_contrast in design_matrix.columns.tolist():
contrast = second_level_contrast
else:
raise ValueError(
f'"{second_level_contrast}" is not a valid contrast name'
)
else:
# Check contrast definition
if second_level_contrast is None:
if design_matrix.shape[1] == 1:
second_level_contrast = np.ones([1])
else:
raise ValueError("No second-level contrast is specified.")
elif (np.nonzero(second_level_contrast)[0]).size != 1:
raise ValueError(
"second_level_contrast must be a list of 0s and 1s."
)
con_val = np.asarray(second_level_contrast, dtype=bool)
contrast = np.asarray(design_matrix.columns.tolist())[con_val][0]
return contrast


def _infer_effect_maps(second_level_input, contrast_def):
"""Deal with the different possibilities of second_level_input."""
# Build the design matrix X and list of imgs Y for GLM fit
Expand Down Expand Up @@ -970,7 +949,7 @@ def non_parametric_inference(
)

# Check and obtain the contrast
contrast = _get_contrast(second_level_contrast, design_matrix)
contrast = _get_con_val(second_level_contrast, design_matrix)
# Get first-level effect_maps
effect_maps = _infer_effect_maps(second_level_input, first_level_contrast)

Expand All @@ -981,9 +960,11 @@ def non_parametric_inference(
var_names = design_matrix.columns.tolist()

# Obtain tested_var
tested_var = np.asarray(design_matrix[contrast])
column_mask = [bool(val) for val in contrast]
tested_var = np.dot(design_matrix, contrast)

# Remove tested var from remaining var names
var_names.remove(contrast)
var_names = [var for var, mask in zip(var_names, column_mask) if not mask]

# Obtain confounding vars
if len(var_names) == 0:
Expand Down
35 changes: 5 additions & 30 deletions nilearn/glm/tests/test_second_level.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@
_check_input_as_first_level_model,
_check_output_type,
_check_second_level_input,
_get_contrast,
_infer_effect_maps,
_process_second_level_input_as_dataframe,
_process_second_level_input_as_firstlevelmodels,
Expand Down Expand Up @@ -420,32 +419,6 @@ def test_check_effect_maps():
_check_effect_maps([1, 2], np.array([[1, 2], [3, 4], [5, 6]]))


def test_get_contrast():
design_matrix = pd.DataFrame([1, 2, 3], columns=["conf"])
assert _get_contrast("conf", design_matrix) == "conf"

design_matrix = pd.DataFrame({"conf1": [1, 2, 3], "conf2": [4, 5, 6]})
assert _get_contrast([0, 1], design_matrix) == "conf2"
assert _get_contrast([1, 0], design_matrix) == "conf1"


def test_get_contrast_errors():
design_matrix = pd.DataFrame([1, 2, 3], columns=["conf"])
with pytest.raises(ValueError, match='"foo" is not a valid contrast name'):
_get_contrast("foo", design_matrix)

design_matrix = pd.DataFrame({"conf1": [1, 2, 3], "conf2": [4, 5, 6]})
with pytest.raises(
ValueError, match="No second-level contrast is specified."
):
_get_contrast(None, design_matrix)
with pytest.raises(
ValueError,
match="second_level_contrast must be a list of 0s and 1s",
):
_get_contrast([0, 0], design_matrix)


def test_infer_effect_maps(tmp_path):
shapes, rk = (SHAPE, (7, 8, 7, 16)), 3
mask, fmri_data, design_matrices = write_fake_fmri_data_and_design(
Expand Down Expand Up @@ -1217,7 +1190,9 @@ def test_non_parametric_inference_contrast_computation(tmp_path):
)


@pytest.mark.parametrize("second_level_contrast", [[1, 0], "r1"])
@pytest.mark.parametrize(
"second_level_contrast", [[1, 0], "r1", "r1-r2", [-1, 1]]
)
def test_non_parametric_inference_contrast_formula(
tmp_path, second_level_contrast, rng
):
Expand Down Expand Up @@ -1253,7 +1228,7 @@ def test_non_parametric_inference_contrast_computation_errors(tmp_path, rng):
# passing null contrast should give back a value error
with pytest.raises(
ValueError,
match=("second_level_contrast must be a list of 0s and 1s."),
match=("Second_level_contrast must be a valid"),
):
non_parametric_inference(
second_level_input=Y,
Expand All @@ -1263,7 +1238,7 @@ def test_non_parametric_inference_contrast_computation_errors(tmp_path, rng):
)
with pytest.raises(
ValueError,
match=("second_level_contrast must be a list of 0s and 1s."),
match=("Second_level_contrast must be a valid"),
):
non_parametric_inference(
second_level_input=Y,
Expand Down