Skip to content

Commit

Permalink
[FIX] rename contrast_type to stat_type (#4191)
Browse files Browse the repository at this point in the history
* rename contrast type to stat type

* mv import

* add test for deprecation

* rename contrast type attribute

* update changelog

* update doc

* update deprecated version

* [misc] fix directive that did not render in doc

* resolve botched merged
  • Loading branch information
Remi-Gau committed Jan 9, 2024
1 parent 91e59cc commit 44b2d6f
Show file tree
Hide file tree
Showing 8 changed files with 118 additions and 61 deletions.
1 change: 1 addition & 0 deletions doc/changes/latest.rst
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ Changes
- :bdg-secondary:`Maint` Switch to using tox to manage environments during development and testing. All plotting python dependencies (matplotlib AND plotly) are now installed when running ``pip install nilearn[plotting]`` (:gh:`4029` by `Rémi Gau`_).
- :bdg-dark:`Code` Private utility context manager ``write_tmp_imgs`` is refactored into function ``write_imgs_to_path`` (:gh:`4094` by `Yasmin Mzayek`_).
- :bdg-dark:`Code` Move user facing function ``concat_niimgs`` out of private module ``nilearn._utils.niimg_conversions`` (:gh:`4167` by `Rémi Gau`_).
- :bdg-danger:`Deprecation` Rename the parameter ``contrast_type`` in :func:`~glm.compute_contrast` and attribute ``contrast_type`` in :class:`~glm.Contrast` to ``stat_type`` (:gh:`4191` by `Rémi Gau`_).
- :bdg-danger:`Deprecation` :func:`~plotting.plot_surf_roi` will raise a warning if ``roi_map`` contains negative or non-integer values; in version 0.13 this will be a ``ValueError`` (:gh:`4131` by `Michelle Wang`_).
- :bdg-dark:`Code` Remove leading underscore from non private functions to align with PEP8 (:gh:`4086` by `Rémi Gau`_).
- :bdg-dark:`Code` Make ``decoding/proximal_operator`` explicitly private to align with PEP8 (:gh:`4153` by `Rémi Gau`_).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@
f"{contrast_id}, right hemisphere")
# compute contrast-related statistics
contrast = compute_contrast(labels, estimates, contrast_val,
contrast_type='t')
stat_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,
Expand Down Expand Up @@ -219,7 +219,7 @@
f"{contrast_id}, left hemisphere")
# compute contrasts
contrast = compute_contrast(labels, estimates, contrast_val,
contrast_type='t')
stat_type='t')
z_score = contrast.z_score()
# plot the result
plotting.plot_surf_stat_map(
Expand Down
4 changes: 2 additions & 2 deletions examples/07_advanced/plot_surface_bids_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@
# 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')
stat_type='t')
# We present the Z-transform of the t map.
z_score = contrast.z_score()
z_scores_right.append(z_score)
Expand All @@ -128,7 +128,7 @@
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')
stat_type='t')
z_scores_left.append(contrast.z_score())

# %%
Expand Down
12 changes: 6 additions & 6 deletions nilearn/glm/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ def positive_reciprocal(X):
return np.where(X <= 0, 0, 1.0 / X)


def pad_contrast(con_val, theta, contrast_type):
def pad_contrast(con_val, theta, stat_type):
"""Pad contrast with zeros if necessary.
If the contrast is shorter than the number of parameters,
Expand All @@ -225,17 +225,17 @@ def pad_contrast(con_val, theta, contrast_type):
theta of RegressionResults instances
where P is the total number of regressors in the design matrix.
contrast_type : {'t', 'F'}, optional
stat_type : {'t', 'F'}, optional
Type of the :term:`contrast`.
"""
nb_cols = con_val.shape[0] if con_val.ndim == 1 else con_val.shape[1]
if nb_cols > theta.shape[0]:
if contrast_type == "t":
if stat_type == "t":
raise ValueError(
f"t contrasts should be of length P={theta.shape[0]}, "
f"but it has length {nb_cols}."
)
if contrast_type == "F":
if stat_type == "F":
raise ValueError(
f"F contrasts should have {theta.shape[0]} columns, "
f"but it has {nb_cols}."
Expand All @@ -244,13 +244,13 @@ def pad_contrast(con_val, theta, contrast_type):
pad = False
if nb_cols < theta.shape[0]:
pad = True
if contrast_type == "t":
if stat_type == "t":
warn(
f"t contrasts should be of length P={theta.shape[0]}, "
f"but it has length {nb_cols}. "
"The rest of the contrast was padded with zeros."
)
if contrast_type == "F":
if stat_type == "F":
warn(
f"F contrasts should have {theta.shape[0]} colmuns, "
f"but it has only {nb_cols}. "
Expand Down
108 changes: 71 additions & 37 deletions nilearn/glm/contrasts.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import pandas as pd
import scipy.stats as sps

from nilearn._utils import rename_parameters
from nilearn.glm._utils import pad_contrast, z_score
from nilearn.maskers import NiftiMasker

Expand Down Expand Up @@ -47,7 +48,10 @@ def expression_to_contrast_vector(expression, design_columns):
return contrast_vector


def compute_contrast(labels, regression_result, con_val, contrast_type=None):
@rename_parameters(
replacement_params={"contrast_type": "stat_type"}, end_version="0.13.0"
)
def compute_contrast(labels, regression_result, con_val, stat_type=None):
"""Compute the specified :term:`contrast` given an estimated glm.
Parameters
Expand All @@ -63,11 +67,17 @@ def compute_contrast(labels, regression_result, con_val, contrast_type=None):
Where q = number of :term:`contrast` vectors
and p = number of regressors.
contrast_type : {None, 't', 'F'}, optional
stat_type : {None, 't', 'F'}, optional
Type of the :term:`contrast`.
If None, then defaults to 't' for 1D `con_val`
and 'F' for 2D `con_val`.
contrast_type :
.. deprecated:: 0.11.0
Use ``stat_type`` instead (see above).
Returns
-------
con : Contrast instance,
Expand All @@ -80,17 +90,17 @@ def compute_contrast(labels, regression_result, con_val, contrast_type=None):
if con_val.ndim > 1:
dim = con_val.shape[0]

if contrast_type is None:
contrast_type = "t" if dim == 1 else "F"
if stat_type is None:
stat_type = "t" if dim == 1 else "F"

acceptable_contrast_types = ["t", "F"]
if contrast_type not in acceptable_contrast_types:
acceptable_stat_types = ["t", "F"]
if stat_type not in acceptable_stat_types:
raise ValueError(
f"'{contrast_type}' is not a known contrast type. "
f"Allowed types are {acceptable_contrast_types}."
f"'{stat_type}' is not a known contrast type. "
f"Allowed types are {acceptable_stat_types}."
)

if contrast_type == "t":
if stat_type == "t":
effect_ = np.zeros((1, labels.size))
var_ = np.zeros(labels.size)
for label_ in regression_result:
Expand All @@ -99,7 +109,7 @@ def compute_contrast(labels, regression_result, con_val, contrast_type=None):
effect_[:, label_mask] = reg.effect.T
var_[label_mask] = (reg.sd**2).T

elif contrast_type == "F":
elif stat_type == "F":
from scipy.linalg import sqrtm

effect_ = np.zeros((dim, labels.size))
Expand All @@ -112,7 +122,7 @@ def compute_contrast(labels, regression_result, con_val, contrast_type=None):
label_mask = labels == label_
reg = regression_result[label_]
con_val = pad_contrast(
con_val=con_val, theta=reg.theta, contrast_type=contrast_type
con_val=con_val, theta=reg.theta, stat_type=stat_type
)
cbeta = np.atleast_2d(np.dot(con_val, reg.theta))
invcov = np.linalg.inv(
Expand All @@ -129,13 +139,11 @@ def compute_contrast(labels, regression_result, con_val, contrast_type=None):
variance=var_,
dim=dim,
dof=dof_,
contrast_type=contrast_type,
contrast_type=stat_type,
)


def compute_fixed_effect_contrast(
labels, results, con_vals, contrast_type=None
):
def compute_fixed_effect_contrast(labels, results, con_vals, stat_type=None):
"""Compute the summary contrast assuming fixed effects.
Adds the same contrast applied to all labels and results lists.
Expand All @@ -147,7 +155,7 @@ def compute_fixed_effect_contrast(
if np.all(con_val == 0):
warn(f"Contrast for session {int(i)} is null.")
continue
contrast_ = compute_contrast(lab, res, con_val, contrast_type)
contrast_ = compute_contrast(lab, res, con_val, stat_type)
contrast = contrast_ if contrast is None else contrast + contrast_
n_contrasts += 1
if contrast is None:
Expand All @@ -169,13 +177,16 @@ class Contrast:
may lead to memory breakage).
"""

@rename_parameters(
replacement_params={"contrast_type": "stat_type"}, end_version="0.13.0"
)
def __init__(
self,
effect,
variance,
dim=None,
dof=DEF_DOFMAX,
contrast_type="t",
stat_type="t",
tiny=DEF_TINY,
dofmax=DEF_DOFMAX,
):
Expand All @@ -195,9 +206,15 @@ def __init__(
dof : scalar, default=DEF_DOFMAX
The degrees of freedom of the residuals.
contrast_type : {'t', 'F'}, default='t'
stat_type : {'t', 'F'}, default='t'
Specification of the :term:`contrast` type.
contrast_type :
.. deprecated:: 0.11.0
Use ``stat_type`` instead (see above).
tiny : float, default=DEF_TINY
Small quantity used to avoid numerical underflows.
Expand All @@ -214,22 +231,39 @@ def __init__(
self.variance = variance
self.dof = float(dof)
self.dim = effect.shape[0] if dim is None else dim
if self.dim > 1 and contrast_type == "t":
if self.dim > 1 and stat_type == "t":
print("Automatically converted multi-dimensional t to F contrast")
contrast_type = "F"
if contrast_type not in ["t", "F"]:
stat_type = "F"
if stat_type not in ["t", "F"]:
raise ValueError(
f"{contrast_type} is not a valid contrast_type. "
"Should be t or F"
f"{stat_type} is not a valid stat_type. " "Should be t or F"
)
self.contrast_type = contrast_type
self.stat_type = stat_type
self.stat_ = None
self.p_value_ = None
self.one_minus_pvalue_ = None
self.baseline = 0
self.tiny = tiny
self.dofmax = dofmax

@property
def contrast_type(self):
"""Return value of stat_type.
.. deprecated:: 0.11.0
"""
attrib_deprecation_msg = (
'The attribute "contrast_type" '
"will be removed in 0.13.0 release of Nilearn. "
'Please use the attribute "stat_type" instead.'
)
warn(
category=DeprecationWarning,
message=attrib_deprecation_msg,
stacklevel=3,
)
return self.stat_type

def effect_size(self):
"""Make access to summary statistics more straightforward \
when computing contrasts."""
Expand Down Expand Up @@ -258,13 +292,13 @@ def stat(self, baseline=0.0):
self.baseline = baseline

# Case: one-dimensional contrast ==> t or t**2
if self.contrast_type == "F":
if self.stat_type == "F":
stat = (
np.sum((self.effect - baseline) ** 2, 0)
/ self.dim
/ np.maximum(self.variance, self.tiny)
)
elif self.contrast_type == "t":
elif self.stat_type == "t":
# avoids division by zero
stat = (self.effect - baseline) / np.sqrt(
np.maximum(self.variance, self.tiny)
Expand Down Expand Up @@ -294,9 +328,9 @@ def p_value(self, baseline=0.0):
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":
if self.stat_type == "t":
p_values = sps.t.sf(self.stat_, np.minimum(self.dof, self.dofmax))
elif self.contrast_type == "F":
elif self.stat_type == "F":
p_values = sps.f.sf(
self.stat_, self.dim, np.minimum(self.dof, self.dofmax)
)
Expand Down Expand Up @@ -326,12 +360,12 @@ def one_minus_pvalue(self, baseline=0.0):
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":
if self.stat_type == "t":
one_minus_pvalues = sps.t.cdf(
self.stat_, np.minimum(self.dof, self.dofmax)
)
else:
assert self.contrast_type == "F"
assert self.stat_type == "F"
one_minus_pvalues = sps.f.cdf(
self.stat_, self.dim, np.minimum(self.dof, self.dofmax)
)
Expand Down Expand Up @@ -370,7 +404,7 @@ def __add__(self, other):
This should be used only on indepndent contrasts.
"""
if self.contrast_type != other.contrast_type:
if self.stat_type != other.stat_type:
raise ValueError(
"The two contrasts do not have consistent type dimensions"
)
Expand All @@ -379,7 +413,7 @@ def __add__(self, other):
"The two contrasts do not have compatible dimensions"
)
dof_ = self.dof + other.dof
if self.contrast_type == "F":
if self.stat_type == "F":
warn("Running approximate fixed effects on F statistics.")
effect_ = self.effect + other.effect
variance_ = self.variance + other.variance
Expand All @@ -388,7 +422,7 @@ def __add__(self, other):
variance=variance_,
dim=self.dim,
dof=dof_,
contrast_type=self.contrast_type,
stat_type=self.stat_type,
)

def __rmul__(self, scalar):
Expand All @@ -401,7 +435,7 @@ def __rmul__(self, scalar):
effect=effect_,
variance=variance_,
dof=dof_,
contrast_type=self.contrast_type,
stat_type=self.stat_type,
)

__mul__ = __rmul__
Expand Down Expand Up @@ -539,20 +573,20 @@ def _compute_fixed_effects_params(
fixed_fx_variance = np.mean(variances, 0) / len(variances)
fixed_fx_contrasts = np.mean(contrasts, 0)
dim = 1
contrast_type = "t"
stat_type = "t"
fixed_fx_contrasts_ = fixed_fx_contrasts
if len(fixed_fx_contrasts.shape) == 2:
dim = fixed_fx_contrasts.shape[0]
if dim > 1:
contrast_type = "F"
stat_type = "F"
else:
fixed_fx_contrasts_ = fixed_fx_contrasts[np.newaxis]
con = Contrast(
effect=fixed_fx_contrasts_,
variance=fixed_fx_variance,
dim=dim,
dof=np.sum(dofs),
contrast_type=contrast_type,
stat_type=stat_type,
)
fixed_fx_z_score = con.z_score()
fixed_fx_stat = con.stat_
Expand Down
1 change: 1 addition & 0 deletions nilearn/glm/first_level/first_level.py
Original file line number Diff line number Diff line change
Expand Up @@ -1114,6 +1114,7 @@ def first_level_from_bids(
sub_labels : :obj:`list` of :obj:`str`, optional
Specifies the subset of subject labels to model.
If 'None', will model all subjects in the dataset.
.. versionadded:: 0.10.1
img_filters : :obj:`list` of :obj:`tuple` (str, str), optional
Expand Down

0 comments on commit 44b2d6f

Please sign in to comment.