docs: def _load_tsv_variables(layout, suffix, dataset=None, columns=None,
                          prepend_type=False, scope='all', **selectors):
      ''' Reads variables from scans.tsv, sessions.tsv, and participants.tsv.
  
      Args:
          layout (BIDSLayout): The BIDSLayout to use.
          suffix (str): The suffix of file to read from. Must be one of 'scans',
              'sessions', or 'participants'.
          dataset (NodeIndex): A BIDS NodeIndex container. If None, a new one is
              initialized.
          columns (list): Optional list of names specifying which columns in the
              files to return. If None, all columns are returned.
          prepend_type (bool): If True, variable names are prepended with the
              type name (e.g., 'age' becomes 'participants.age').
          scope (str, list): The scope of the space to search for variables. See
              docstring for BIDSLayout for details and valid predefined values.
          selectors (dict): Optional keyword arguments passed onto the
              BIDSLayout instance's get() method; can be used to constrain
              which data are loaded.
  
      Returns: A NodeIndex instance.
      '''
  
      # Sanitize the selectors: only keep entities at current level or above
      remap = {'scans': 'run', 'sessions': 'session', 'participants': 'subject'}
      level = remap[suffix]
      valid_entities = BASE_ENTITIES[:BASE_ENTITIES.index(level)]
      layout_kwargs = {k: v for k, v in selectors.items() if k in valid_entities}
  
      if dataset is None:
          dataset = NodeIndex()
  
      files = layout.get(extensions='.tsv', return_type='file', suffix=suffix,
                         scope=scope, **layout_kwargs)
  
      for f in files:
  
          f = layout.files[f]
          _data = pd.read_csv(f.path, sep='\t')
  
          # Entities can be defined either within the first column of the .tsv
          # file (for entities that vary by row), or from the full file path
          # (for entities constant over all rows in the file). We extract both
          # and store them in the main DataFrame alongside other variables (as
          # they'll be extracted when the BIDSVariable is initialized anyway).
          for ent_name, ent_val in f.entities.items():
              if ent_name in ALL_ENTITIES:
                  _data[ent_name] = ent_val
  
          # Handling is a bit more convoluted for scans.tsv, because the first
          # column contains the run filename, which we also need to parse.
          if suffix == 'scans':
  
              # Suffix is guaranteed to be present in each filename, so drop the
              # constant column with value 'scans' to make way for it and prevent
              # two 'suffix' columns.
              _data.drop(columns='suffix', inplace=True)
  
              image = _data['filename']
              _data = _data.drop('filename', axis=1)
              dn = f.dirname
              paths = [join(dn, p) for p in image.values]
              ent_recs = [layout.files[p].entities for p in paths
                          if p in layout.files]
              ent_cols = pd.DataFrame.from_records(ent_recs)
              _data = pd.concat([_data, ent_cols], axis=1, sort=True)
              # It's possible to end up with duplicate entity columns this way
              _data = _data.T.drop_duplicates().T
  
          # The BIDS spec requires ID columns to be named 'session_id', 'run_id',
          # etc., and IDs begin with entity prefixes (e.g., 'sub-01'). To ensure
          # consistent internal handling, we strip these suffixes and prefixes.
          elif suffix == 'sessions':
              _data = _data.rename(columns={'session_id': 'session'})
              _data['session'] = _data['session'].str.replace('ses-', '')
          elif suffix == 'participants':
              _data = _data.rename(columns={'participant_id': 'subject'})
              _data['subject'] = _data['subject'].str.replace('sub-', '')
  
          def make_patt(x, regex_search=False):
              patt = '%s' % x
              if isinstance(x, (int, float)):
                  # allow for leading zeros if a number was specified
                  # regardless of regex_search
                  patt = '0*' + patt
              if not regex_search:
                  patt = '^%s$' % patt
              return patt
  
          # Filter rows on all selectors
          comm_cols = list(set(_data.columns) & set(selectors.keys()))
          for col in comm_cols:
              ent_patts = [make_patt(x, regex_search=layout.regex_search)
                              for x in listify(selectors.get(col))]
              patt = '|'.join(ent_patts)
  
              _data = _data[_data[col].str.contains(patt)]
  
          level = {'scans': 'session', 'sessions': 'subject',
                   'participants': 'dataset'}[suffix]
          node = dataset.get_or_create_node(level, f.entities)
  
          ent_cols = list(set(ALL_ENTITIES) & set(_data.columns))
          amp_cols = list(set(_data.columns) - set(ent_cols))
  
          if columns is not None:
              amp_cols = list(set(amp_cols) & set(columns))
  
          for col_name in amp_cols:
  
              # Rename colummns: values must be in 'amplitude'
              df = _data.loc[:, [col_name] + ent_cols]
              df.columns = ['amplitude'] + ent_cols
  
              if prepend_type:
                  col_name = '%s.%s' % (suffix, col_name)
  
              node.add_variable(SimpleVariable(name=col_name, data=df, source=suffix))
  
      return dataset