Dealing with missing trials per run in nilearn.FirstLevelModel

I just started switching some of my fmri glm analyses from FSL to nilearn.glm - So far, I’m really happy with it!

Right now, I’m trying to run a simple two stage GLM contrasting two trial_types. However, our experiment has several runs per subject and one of the trial_types does not occur in all of the runs. Hence, not all our design matrices (generated with make_first_level_design_matrix) have the same number of regressors and computing a contrast with the FirstLevelModel.compute_contrast() method fails.

As far as I know, FSL has a method to use “Null EVs” for such scenarios. Is there anything comparable in nilearn?

Cheers and many thanks

You can specific your contrasts in a symbolic form, as “A-B”, where ‘A’ and 'B 'are the event-type (condition) names, instead of a sequence of 0’s and 1’s. Does the use of this solve your issue ?
HTH
Bertrand

(apologies for reviving an old thread)
I’m running into the same issue, where one trial type does not occur in all runs—I’m contrasting fb_correct and fb_wrong, and some participants have runs where they get every trial correct and so don’t receive the incorrect feedback in that run.

contrast_def is defined as 'fb_correct - fb_wrong'

Thoughts on how to address this in nilearn? (currently running 0.9.2)

Here’s the whole traceback:

KeyError: 'fb_wrong'

The above exception was the direct cause of the following exception:

UndefinedVariableError                    Traceback (most recent call last)
Input In [30], in <cell line: 1>()
      4 stim_list, models, models_run_imgs, \
      5     models_events, models_confounds, conf_keep_list = prep_models_and_args(subject_id, task_label, 
      6                                                                              fwhm, bidsroot, 
      7                                                                              deriv_dir,
      8                                                                              t_r, 
      9                                                                              space_label)
     11 # Across-run GLM
---> 12 nilearn_glm_across_runs(stim_list, task_label, 
     13                         models, models_run_imgs, 
     14                         models_events, models_confounds, 
     15                         conf_keep_list, space_label)

Input In [29], in nilearn_glm_across_runs(stim_list, task_label, models, models_run_imgs, models_events, models_confounds, conf_keep_list, space_label)
     28 # compute the contrast of interest
     29 print('computing contrast of interest')
---> 30 summary_statistics = model.compute_contrast('fb_correct - fb_wrong', output_type='all')
     31 zmap = summary_statistics['z_score']
     32 tmap = summary_statistics['stat']

File /bgfs/bchandrasekaran/krs228/software/miniconda3/envs/py3/lib/python3.9/site-packages/nilearn/glm/first_level/first_level.py:708, in FirstLevelModel.compute_contrast(self, contrast_def, stat_type, output_type)
    706     design_columns = design_mat.columns.tolist()
    707     if isinstance(con, str):
--> 708         con_vals[cidx] = expression_to_contrast_vector(
    709             con, design_columns)
    711 valid_types = ['z_score', 'stat', 'p_value', 'effect_size',
    712                'effect_variance']
    713 valid_types.append('all')  # ensuring 'all' is the final entry.

File /bgfs/bchandrasekaran/krs228/software/miniconda3/envs/py3/lib/python3.9/site-packages/nilearn/glm/contrasts.py:43, in expression_to_contrast_vector(expression, design_columns)
     41     return contrast_vector
     42 df = pd.DataFrame(np.eye(len(design_columns)), columns=design_columns)
---> 43 contrast_vector = df.eval(expression, engine="python").values
     44 return contrast_vector

File /bgfs/bchandrasekaran/krs228/software/miniconda3/envs/py3/lib/python3.9/site-packages/pandas/core/frame.py:3599, in DataFrame.eval(self, expr, inplace, **kwargs)
   3596     kwargs["target"] = self
   3597 kwargs["resolvers"] = kwargs.get("resolvers", ()) + tuple(resolvers)
-> 3599 return _eval(expr, inplace=inplace, **kwargs)

File /bgfs/bchandrasekaran/krs228/software/miniconda3/envs/py3/lib/python3.9/site-packages/pandas/core/computation/eval.py:342, in eval(expr, parser, engine, truediv, local_dict, global_dict, resolvers, level, target, inplace)
    333 # get our (possibly passed-in) scope
    334 env = ensure_scope(
    335     level + 1,
    336     global_dict=global_dict,
   (...)
    339     target=target,
    340 )
--> 342 parsed_expr = Expr(expr, engine=engine, parser=parser, env=env)
    344 # construct the engine and evaluate the parsed expression
    345 eng = ENGINES[engine]

File /bgfs/bchandrasekaran/krs228/software/miniconda3/envs/py3/lib/python3.9/site-packages/pandas/core/computation/expr.py:798, in Expr.__init__(self, expr, engine, parser, env, level)
    796 self.parser = parser
    797 self._visitor = PARSERS[parser](self.env, self.engine, self.parser)
--> 798 self.terms = self.parse()

File /bgfs/bchandrasekaran/krs228/software/miniconda3/envs/py3/lib/python3.9/site-packages/pandas/core/computation/expr.py:817, in Expr.parse(self)
    813 def parse(self):
    814     """
    815     Parse an expression.
    816     """
--> 817     return self._visitor.visit(self.expr)

File /bgfs/bchandrasekaran/krs228/software/miniconda3/envs/py3/lib/python3.9/site-packages/pandas/core/computation/expr.py:401, in BaseExprVisitor.visit(self, node, **kwargs)
    399 method = "visit_" + type(node).__name__
    400 visitor = getattr(self, method)
--> 401 return visitor(node, **kwargs)

File /bgfs/bchandrasekaran/krs228/software/miniconda3/envs/py3/lib/python3.9/site-packages/pandas/core/computation/expr.py:407, in BaseExprVisitor.visit_Module(self, node, **kwargs)
    405     raise SyntaxError("only a single expression is allowed")
    406 expr = node.body[0]
--> 407 return self.visit(expr, **kwargs)

File /bgfs/bchandrasekaran/krs228/software/miniconda3/envs/py3/lib/python3.9/site-packages/pandas/core/computation/expr.py:401, in BaseExprVisitor.visit(self, node, **kwargs)
    399 method = "visit_" + type(node).__name__
    400 visitor = getattr(self, method)
--> 401 return visitor(node, **kwargs)

File /bgfs/bchandrasekaran/krs228/software/miniconda3/envs/py3/lib/python3.9/site-packages/pandas/core/computation/expr.py:410, in BaseExprVisitor.visit_Expr(self, node, **kwargs)
    409 def visit_Expr(self, node, **kwargs):
--> 410     return self.visit(node.value, **kwargs)

File /bgfs/bchandrasekaran/krs228/software/miniconda3/envs/py3/lib/python3.9/site-packages/pandas/core/computation/expr.py:401, in BaseExprVisitor.visit(self, node, **kwargs)
    399 method = "visit_" + type(node).__name__
    400 visitor = getattr(self, method)
--> 401 return visitor(node, **kwargs)

File /bgfs/bchandrasekaran/krs228/software/miniconda3/envs/py3/lib/python3.9/site-packages/pandas/core/computation/expr.py:522, in BaseExprVisitor.visit_BinOp(self, node, **kwargs)
    521 def visit_BinOp(self, node, **kwargs):
--> 522     op, op_class, left, right = self._maybe_transform_eq_ne(node)
    523     left, right = self._maybe_downcast_constants(left, right)
    524     return self._maybe_evaluate_binop(op, op_class, left, right)

File /bgfs/bchandrasekaran/krs228/software/miniconda3/envs/py3/lib/python3.9/site-packages/pandas/core/computation/expr.py:444, in BaseExprVisitor._maybe_transform_eq_ne(self, node, left, right)
    442     left = self.visit(node.left, side="left")
    443 if right is None:
--> 444     right = self.visit(node.right, side="right")
    445 op, op_class, left, right = self._rewrite_membership_op(node, left, right)
    446 return op, op_class, left, right

File /bgfs/bchandrasekaran/krs228/software/miniconda3/envs/py3/lib/python3.9/site-packages/pandas/core/computation/expr.py:401, in BaseExprVisitor.visit(self, node, **kwargs)
    399 method = "visit_" + type(node).__name__
    400 visitor = getattr(self, method)
--> 401 return visitor(node, **kwargs)

File /bgfs/bchandrasekaran/krs228/software/miniconda3/envs/py3/lib/python3.9/site-packages/pandas/core/computation/expr.py:535, in BaseExprVisitor.visit_Name(self, node, **kwargs)
    534 def visit_Name(self, node, **kwargs):
--> 535     return self.term_type(node.id, self.env, **kwargs)

File /bgfs/bchandrasekaran/krs228/software/miniconda3/envs/py3/lib/python3.9/site-packages/pandas/core/computation/ops.py:86, in Term.__init__(self, name, env, side, encoding)
     84 tname = str(name)
     85 self.is_local = tname.startswith(LOCAL_TAG) or tname in DEFAULT_GLOBALS
---> 86 self._value = self._resolve_name()
     87 self.encoding = encoding

File /bgfs/bchandrasekaran/krs228/software/miniconda3/envs/py3/lib/python3.9/site-packages/pandas/core/computation/ops.py:103, in Term._resolve_name(self)
    102 def _resolve_name(self):
--> 103     res = self.env.resolve(self.local_name, is_local=self.is_local)
    104     self.update(res)
    106     if hasattr(res, "ndim") and res.ndim > 2:

File /bgfs/bchandrasekaran/krs228/software/miniconda3/envs/py3/lib/python3.9/site-packages/pandas/core/computation/scope.py:217, in Scope.resolve(self, key, is_local)
    213 except KeyError as err:
    214     # runtime import because ops imports from scope
    215     from pandas.core.computation.ops import UndefinedVariableError
--> 217     raise UndefinedVariableError(key, is_local) from err

UndefinedVariableError: name 'fb_wrong' is not defined

I have the same issue.
I solved it by checking the presence of the target event in the runs before adding them to the list I pass to the GLM. In my case it works well because I have only one event, and some runs just don’t have it so I can drop them.
There might be a more elegant and general fix but I couldn’t find it yet…

Edit :
Another idea could be to manually create an additional column in the design matrix of the run that misses your event, and fill it with zeros (keep it empty if that works).

1 Like