docs: def to_df(self, timing=True, metadata=False, format='wide',
                extractor_name=False, object_id=True, **to_df_kwargs):
          ''' Convert current instance to a pandas DatasFrame.
  
          Args:
              timing (bool): If True, adds columns for event onset and duration.
                  Note that these columns will be added even if there are no
                  valid values in the current object (NaNs will be inserted).
                  If 'auto', timing columns are only inserted if there's at least
                  one valid (i.e., non-NaN) onset/order/duration.
              metadata (bool): If True, adds columns for key metadata (including
                  the name, filename, class, history, and source file of the
                  Stim).
              format (str): Format to return the data in. Can be either 'wide' or
                  'long'. In the wide case, every extracted feature is a column,
                  and every result object is in a row. In the long case, every
                  row contains a single record/feature combination.
              extractor_name (bool): If True, includes the Extractor name as
                  a column (in 'long' format) or index level (in 'wide' format).
              object_id (bool): If True, attempts to intelligently add an
                  'object_id' column that differentiates between multiple objects
                  in the results that may share onsets and durations (and would
                  otherwise be impossible to distinguish). This frequently occurs
                  for ImageExtractors that identify multiple target objects
                  (e.g., faces) within a single ImageStim. In addition to boolean
                  values, the special value 'auto' can be passed, in which case
                  the object_id column will only be inserted if the resulting
                  constant would be non-constant.
  
          Returns:
              A pandas DataFrame.
          '''
  
          # Ideally, Extractors should implement their own _to_df() class method
          # that produces a DataFrame in standardized format. Failing that, we
          # assume self._data is already array-like and can be wrapped in a DF.
          if hasattr(self.extractor, '_to_df'):
              df = self.extractor._to_df(self, **to_df_kwargs)
          else:
              features = self.features
              data = np.array(self._data)
              if features is None:
                  features = ['feature_%d' % (i + 1)
                              for i in range(data.shape[1])]
              df = pd.DataFrame(data, columns=features)
  
          if hasattr(self, '_onsets'):
              onsets = np.array(self._onsets)
              onsets += 0.0 if self.onset is None else self.onset
          else:
              onsets = np.nan if self.onset is None else self.onset
          durations = getattr(self, '_durations', self.duration)
          durations = np.nan if durations is None else durations
          orders = getattr(self, '_orders', self.order)
          orders = np.nan if orders is None else orders
  
          # If any features clash with protected keys, append underscore
          protected = ['onset', 'order', 'duration', 'extractor', 'stim_name', \
                       'class', 'filename', 'history', 'source_file']
          df = df.rename(columns={k: k + '_' for k in protected})
  
          index_cols = []
  
          # Generally we leave it to Extractors to properly track the number of
          # objects returned in the result DF, using the 'object_id' column.
          # But in cases where the Extractor punt on this and_object_id=True, we
          # take our best guess. The logic is that we increment the object
          # counter for any row in the DF that cannot be uniquely distinguished
          # from other rows by onset and duration.
          if object_id and 'object_id' not in df.columns:
              index = pd.Series(onsets).astype(str) + '_' + \
                  pd.Series(durations).astype(str)
              if object_id is True or (object_id == 'auto' and
                                       len(set(index)) < len(df)):
                  ids = np.arange(len(df)) if len(index) == 1 \
                      else df.groupby(index).cumcount()
                  df.insert(0, 'object_id', ids)
                  index_cols = ['object_id']
  
          if timing is True or (timing == 'auto' and
                                (np.isfinite(durations).any() or
                                 np.isfinite(orders).any())):
              df.insert(0, 'onset', onsets)
              df.insert(0, 'duration', durations)
              df.insert(0, 'order', orders)
              df = df.sort_values('onset').reset_index(drop=True)
              index_cols.extend(['onset', 'order', 'duration'])
  
          if format == 'long':
              df = df.melt(index_cols, var_name='feature')
              df = df.dropna(subset=['value'])
  
          if extractor_name:
              name = self.extractor.name
              if format == 'long':
                  df['extractor'] = name
              else:
                  df.columns = pd.MultiIndex.from_product([[name], df.columns])
  
          if metadata:
              df['stim_name'] = self.stim.name
              df['class'] = self.stim.__class__.__name__
              df['filename'] = self.stim.filename
              hist = '' if self.stim.history is None else str(self.stim.history)
              df['history'] = hist
              df['source_file'] = self.history.to_df().iloc[0].source_file
          return df