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 pandas.Series as valid input for second_level_input #4070

Merged
merged 5 commits into from
Oct 23, 2023
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
2 changes: 1 addition & 1 deletion doc/changes/latest.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,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`_).

Changes
Expand Down
6 changes: 4 additions & 2 deletions nilearn/_utils/docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -795,8 +795,10 @@ def custom_function(vertices):
"second_level_input"
] = """
second_level_input : :obj:`list` of \
:class:`~nilearn.glm.first_level.FirstLevelModel` objects \
or :class:`pandas.DataFrame` or :obj:`list` of Niimg-like objects.
:class:`~nilearn.glm.first_level.FirstLevelModel` objects or \
:class:`pandas.DataFrame` or \
:obj:`list` of Niimg-like objects or \
:obj:`pandas.Series` of Niimg-like objects.

- Giving :class:`~nilearn.glm.first_level.FirstLevelModel` objects
will allow to easily compute the second level contrast of arbitrary first
Expand Down
8 changes: 7 additions & 1 deletion nilearn/glm/second_level/second_level.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ def _check_input_type(second_level_input):
"""Determine the type of input provided."""
if isinstance(second_level_input, pd.DataFrame):
return "df_object"
if isinstance(second_level_input, pd.Series):
return "pd_series"
if isinstance(second_level_input, (str, Nifti1Image)):
return "nii_object"
if isinstance(second_level_input, list):
Expand All @@ -56,6 +58,7 @@ def _check_input_type(second_level_input):
"second_level_input must be "
"either a pandas DataFrame, "
"a Niimg-like object, "
"a pandas Series of Niimg-like object, "
"a list of Niimg-like object or "
"a list of FirstLevelModel objects. "
f"Got {_return_type(second_level_input)} instead"
Expand Down Expand Up @@ -105,6 +108,9 @@ def _check_input_as_type(
):
if input_type == "flm_object":
_check_input_as_first_level_model(second_level_input, none_confounds)
elif input_type == "pd_series":
second_level_input = second_level_input.to_list()
_check_input_as_nifti_images(second_level_input, none_design_matrix)
elif input_type == "nii_object":
_check_input_as_nifti_images(second_level_input, none_design_matrix)
else:
Expand Down Expand Up @@ -180,7 +186,7 @@ def _check_input_as_dataframe(second_level_input):
raise ValueError(
"second_level_input DataFrame must have"
" columns subject_label, map_name and"
" effects_map_path"
" effects_map_path."
)
if not all(
isinstance(_, str)
Expand Down
61 changes: 50 additions & 11 deletions nilearn/glm/tests/test_second_level.py
Original file line number Diff line number Diff line change
Expand Up @@ -624,6 +624,26 @@ def test_fmri_inputs(tmp_path, rng):
SecondLevelModel().fit(niimg_4d, None, sdes)


def test_fmri_pandas_series_as_input(tmp_path, rng):
# prepare correct input dataframe and lists
p, q = 80, 10
X = rng.standard_normal(size=(p, q))
shapes = (SHAPE,)
_, FUNCFILE, _ = write_fake_fmri_data_and_design(
shapes, file_path=tmp_path
)
FUNCFILE = FUNCFILE[0]

# dataframes as input
sdes = pd.DataFrame(X[:3, :3], columns=["intercept", "b", "c"])
niidf = pd.DataFrame({"filepaths": [FUNCFILE, FUNCFILE, FUNCFILE]})
SecondLevelModel().fit(
second_level_input=niidf["filepaths"],
confounds=None,
design_matrix=sdes,
)


def test_fmri_inputs_errors(tmp_path):
# Test processing of FMRI inputs
# prepare fake data
Expand Down Expand Up @@ -658,17 +678,6 @@ def test_fmri_inputs_errors(tmp_path):
with pytest.raises(TypeError, match="at least two"):
SecondLevelModel().fit([flm])

# test dataframe requirements
dfcols = ["subject_label", "map_name", "effects_map_path"]
dfrows = [
["01", "a", FUNCFILE],
["02", "a", FUNCFILE],
["03", "a", FUNCFILE],
]
niidf = pd.DataFrame(dfrows, columns=dfcols)
with pytest.raises(TypeError, match="second_level_input must be"):
SecondLevelModel().fit(niidf["subject_label"])

confounds = pd.DataFrame(
[["01", 1], ["02", 2], ["03", 3]],
columns=["subject_label", "conf1"],
Expand All @@ -693,6 +702,36 @@ def test_fmri_inputs_errors(tmp_path):
SecondLevelModel().fit(flms, None, [])


def test_fmri_inputs_pandas_errors():
# test wrong input for list and pandas requirements
nii_img = ["01", "02", "03"]
with pytest.raises(ValueError, match="File not found: "):
SecondLevelModel().fit(nii_img)

nii_series = pd.Series(nii_img)
with pytest.raises(ValueError, match="File not found: "):
SecondLevelModel().fit(nii_series)

# test dataframe requirements
dfcols = [
"not_the_right_column_name",
]
dfrows = [
["01"],
["02"],
["03"],
]
niidf = pd.DataFrame(dfrows, columns=dfcols)
with pytest.raises(
ValueError,
match=(
"second_level_input DataFrame must have "
"columns subject_label, map_name and effects_map_path."
),
):
SecondLevelModel().fit(niidf)


def test_fmri_inputs_for_non_parametric_inference_errors(tmp_path, rng):
# Test processing of FMRI inputs

Expand Down