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
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.