Returns summary analysis from the dataframe as a DataTable object.
           DataTables are wrapped pandas dataframes which can be combined if the have the same width. This is useful for combining multiple analyses.
           DataTables can be printed to terminal as a tabular string using their representation function (i.e. print(data_table)).
           This function (tabulate) looks at specific analysis; this class (DatasetDataFrame) can be subclassed for custom tabulation.

def tabulate(self, restricted_predicted_column_indices = [], restricted_predicted_column_names = [], dataset_name = None):
        '''Returns summary analysis from the dataframe as a DataTable object.
           DataTables are wrapped pandas dataframes which can be combined if the have the same width. This is useful for combining multiple analyses.
           DataTables can be printed to terminal as a tabular string using their representation function (i.e. print(data_table)).
           This function (tabulate) looks at specific analysis; this class (DatasetDataFrame) can be subclassed for custom tabulation.'''

        self._analyze()

        data_series = self.get_series_names(column_indices = restricted_predicted_column_indices, column_names = restricted_predicted_column_names)

        # Determine the multi-index headers
        group_names = []
        for l in self.index_layers:
            group_names.append(l)

        # Set up the table headers
        headers = ['Dataset'] + group_names + ['n', 'R', 'rho', 'MAE', 'Fraction correct  ', 'FC sign', 'SB sensitivity', 'SB specificity']
        table_rows = []
        for dseries in data_series:
            if isinstance(dseries, tuple):
                dseries_l = list(dseries)
            else:
                assert(isinstance(dseries, basestring))
                dseries_l = [dseries]

            results = []
            assert (len(self.index_layers) == len(dseries))
            if self.analysis.get(dseries, {}).get('partial') and self.analysis.get(dseries, {}).get('full'):# data_series in self.analysis[dseries]['full']:
                results.append((dseries_l[:-1] + [dseries_l[-1] + '*'], self.analysis[dseries]['partial']))
                results.append((dseries_l[:-1] + [dseries_l[-1]], self.analysis[dseries]['full']))
            elif (self.analysis.get(dseries, {}).get('partial')):
                results.append((dseries_l[:-1] + [dseries_l[-1] + '*'], self.analysis[dseries]['partial']))
            elif (self.analysis.get(dseries, {}).get('full')):
                results = [(dseries, self.analysis[dseries]['full'])]

            for result in results:
                n = result[1]['data']['n']
                R = result[1]['data']['pearsonr'][0]
                rho = result[1]['data']['spearmanr'][0]
                mae = result[1]['data']['MAE']
                fraction_correct = result[1]['data']['fraction_correct']
                accuracy = result[1]['data']['accuracy']
                SBSensitivity = '{0:.3f} / {1}'.format(result[1]['data']['significant_beneficient_sensitivity'][0], result[1]['data']['significant_beneficient_sensitivity'][1])
                SBSpecificity = '{0:.3f} / {1}'.format(result[1]['data']['significant_beneficient_specificity'][0], result[1]['data']['significant_beneficient_specificity'][1])

                method = result[0]
                if isinstance(method, tuple):
                    method = list(method)

                table_rows.append([dataset_name or self.reference_dataset_name] + method +
                                  [n, R, rho, mae, fraction_correct, accuracy, SBSensitivity, SBSpecificity])

        # Convert the lists into a (wrapped) pandas dataframe to make use of the pandas formatting code to save reinventing the wheel...
        return DataTable(pandas.DataFrame(table_rows, columns = headers), self.index_layers)