44from collections import namedtuple , OrderedDict , Counter , defaultdict
55import itertools
66from functools import reduce
7+ from multiprocessing .sharedctypes import Value
78import re
89import fnmatch
910
@@ -263,11 +264,11 @@ class BIDSStatsModelsNode:
263264 overridden if one is passed when run() is called on a node.
264265 """
265266
266- def __init__ (self , level , name , transformations = None , model = None ,
267- contrasts = None , dummy_contrasts = False , group_by = None ):
267+ def __init__ (self , level , name , model , group_by , transformations = None ,
268+ contrasts = None , dummy_contrasts = False ):
268269 self .level = level .lower ()
269270 self .name = name
270- self .model = model or {}
271+ self .model = model
271272 if transformations is None :
272273 transformations = {"transformer" : "pybids-transforms-v1" ,
273274 "instructions" : []}
@@ -279,13 +280,7 @@ def __init__(self, level, name, transformations=None, model=None,
279280 self .children = []
280281 self .parents = []
281282 if group_by is None :
282- group_by = []
283- # Loop over contrasts after first level
284- if self .level != "run" :
285- group_by .append ("contrast" )
286- # Loop over node level of this node
287- if self .level != "dataset" :
288- group_by .append (self .level )
283+ raise ValueError (f"group_by is not defined for Node: { name } " )
289284 self .group_by = group_by
290285
291286 # Check for intercept only run level model and throw an error
@@ -592,25 +587,21 @@ def __init__(self, node, entities={}, collections=None, inputs=None,
592587
593588 var_names = list (self .node .model ['x' ])
594589
595- # Handle the special 1 construct. If it's present, we add a
596- # column of 1's to the design matrix. But behavior varies:
597- # * If there's only a single contrast across all of the inputs,
598- # the intercept column is given the same name as the input contrast.
599- # It may already exist, in which case we do nothing.
600- # * Otherwise, we name the column 'intercept'.
601- int_name = None
590+ # Handle the special 1 construct.
591+ # Add column of 1's to the design matrix called "intercept"
602592 if 1 in var_names :
603- if ('contrast' not in df .columns or df ['contrast' ].nunique () > 1 ):
604- int_name = 'intercept'
605- else :
606- int_name = df ['contrast' ].unique ()[0 ]
607-
608- var_names .remove (1 )
609-
610- if int_name not in df .columns :
611- df .insert (0 , int_name , 1 )
612- else :
613- var_names .append (int_name )
593+ if "intercept" in var_names :
594+ raise ValueError ("Cannot define both '1' and 'intercept' in 'X'" )
595+
596+ var_names = ['intercept' if i == 1 else i for i in var_names ]
597+ if 'intercept' not in df .columns :
598+ df .insert (0 , 'intercept' , 1 )
599+
600+ # If a single incoming contrast
601+ if ('contrast' in df .columns and df ['contrast' ].nunique () == 1 ):
602+ unique_in_contrast = df ['contrast' ].unique ()[0 ]
603+ else :
604+ unique_in_contrast = None
614605
615606 var_names = expand_wildcards (var_names , df .columns )
616607
@@ -626,7 +617,7 @@ def __init__(self, node, entities={}, collections=None, inputs=None,
626617
627618 # Create ModelSpec and build contrasts
628619 self .model_spec = create_model_spec (self .data , node .model , self .metadata )
629- self .contrasts = self ._build_contrasts (int_name )
620+ self .contrasts = self ._build_contrasts (unique_in_contrast )
630621
631622 def _collections_to_dfs (self , collections ):
632623 """Merges collections and converts them to a pandas DataFrame."""
@@ -690,17 +681,58 @@ def _inputs_to_df(self, inputs):
690681 input_df .loc [input_df .index [i ], con .name ] = 1
691682 return input_df
692683
693- def _build_contrasts (self , int_name ):
694- """Contrast list of ContrastInfo objects based on current state."""
695- contrasts = {}
684+ def _build_contrasts (self , unique_in_contrast = None ):
685+ """Contrast list of ContrastInfo objects based on current state.
686+
687+ Parameters
688+ ----------
689+ unique_in_contrast : string
690+ Name of unique incoming contrast inputs (i.e. if there is only 1)
691+ """
692+ in_contrasts = self .node .contrasts .copy ()
696693 col_names = set (self .X .columns )
697- for con in self .node .contrasts :
698- name = con ["name" ]
694+
695+ # Create dummy contrasts as regular contrasts
696+ dummies = self .node .dummy_contrasts
697+ if dummies :
698+ if 'conditionlist' in dummies :
699+ conditions = set (dummies ['condition_list' ])
700+ else :
701+ conditions = col_names
702+
703+ for col_name in conditions :
704+ if col_name == "intercept" :
705+ col_name = 1
706+
707+ in_contrasts .insert (0 ,
708+ {
709+ 'name' : col_name ,
710+ 'condition_list' : [col_name ],
711+ 'weights' : [1 ],
712+ 'test' : dummies .get ('test' )
713+ }
714+ )
715+
716+ # Process all contrasts, starting with dummy contrasts
717+ # Dummy contrasts are replaced if a contrast is defined with same name
718+ contrasts = {}
719+ for con in in_contrasts :
699720 condition_list = list (con ["condition_list" ])
700- if 1 in condition_list and int_name is not None :
701- condition_list [condition_list .index (1 )] = int_name
702- if name == 1 and int_name is not None :
703- name = int_name
721+
722+ # Rename special 1 construct
723+ condition_list = ['intercept' if i == 1 else i for i in condition_list ]
724+
725+ name = con ["name" ]
726+
727+ # Rename contrast name
728+ if name == 1 :
729+ name = unique_in_contrast or 'intercept'
730+ else :
731+ # If Node has single contrast input, as is grouped by contrast
732+ # Rename contrast to append incoming contrast name
733+ if unique_in_contrast :
734+ name = f"{ unique_in_contrast } _{ name } "
735+
704736 missing_vars = set (condition_list ) - col_names
705737 if missing_vars :
706738 if self .invalid_contrasts == 'error' :
@@ -711,31 +743,13 @@ def _build_contrasts(self, int_name):
711743 elif self .invalid_contrasts == 'drop' :
712744 continue
713745 weights = np .atleast_2d (con ['weights' ])
746+
714747 # Add contrast name to entities; can be used in grouping downstream
715748 entities = {** self .entities , 'contrast' : name }
716749 ci = ContrastInfo (name , condition_list ,
717750 con ['weights' ], con .get ("test" ), entities )
718751 contrasts [name ] = ci
719752
720- dummies = self .node .dummy_contrasts
721- if dummies :
722- conditions = col_names
723- if 'conditions' in dummies :
724- conds = set (dummies ['conditions' ])
725- if 1 in conds and int_name is not None :
726- conds .discard (1 )
727- conds .add (int_name )
728- conditions &= conds
729- conditions -= set (c .name for c in contrasts .values ())
730-
731- for col_name in conditions :
732- if col_name in contrasts :
733- continue
734- entities = {** self .entities , 'contrast' : col_name }
735- ci = ContrastInfo (col_name , [col_name ], [1 ], dummies .get ("test" ),
736- entities )
737- contrasts [col_name ] = ci
738-
739753 return list (contrasts .values ())
740754
741755 @property
0 commit comments