docs: def calculate_metrics(self, analysis_set = '', analysis_directory = None, drop_missing = True, case_n_cutoff = 5, verbose = True):
          '''Calculates the main metrics for the benchmark run and writes them to file and LaTeX object.'''
  
          dataframe = self.dataframe
          if drop_missing:
              dataframe = dataframe.dropna(subset=['Predicted'])
  
          if self.calculate_scalar_adjustments:
              scalar_adjustment = self.scalar_adjustments[analysis_set]
          experimental_field = BenchmarkRun.get_analysis_set_fieldname('Experimental', analysis_set)
  
          self.metric_latex_objects.append( lr.LatexPageSection('Data tables', None, True) )
          intro_text = lr.LatexText( text = self.ddg_analysis_type_description )
          header_row = ['Statistic name', '{Value}', 'p-value']
          stats_column_format = ['l', 'S[table-format=3.2]', 'l']
  
          if self.include_derived_mutations:
              running_analysis_str = '\nDerived mutations in analysis are included):'
          else:
              running_analysis_str = '\nDerived mutations in analysis are omitted):'
          intro_text.add_text(running_analysis_str)
          if verbose:
              self.report(running_analysis_str, fn = colortext.message)
  
          classification_cutoffs_str = 'The stability classification cutoffs are: Experimental=%0.2f kcal/mol, Predicted=%0.2f energy units.' % (self.stability_classication_x_cutoff, self.stability_classication_y_cutoff)
          intro_text.add_text( classification_cutoffs_str )
          if verbose:
              self.report(classification_cutoffs_str, fn = colortext.warning)
  
          self.metric_latex_objects.append( intro_text )
  
          amino_acid_details, CAA, PAA, HAA = self.amino_acid_details, self.CAA, self.PAA, self.HAA
  
          # This dict is used for the print-statement below
          volume_groups = {}
          for aa_code, aa_details in amino_acid_details.iteritems():
              v = int(aa_details['van der Waals volume']) # Note: I only convert to int here to match the old script behavior and because all volumes are integer values so it does not do any harm
              volume_groups[v] = volume_groups.get(v, [])
              volume_groups[v].append(aa_code)
  
          section_latex_objs = []
          section_latex_objs.append( lr.LatexSubSection(
              'Breakdown by volume',
              'A case is considered a small-to-large (resp. large-to-small) mutation if all of the wildtype residues have a smaller (resp. larger) van der Waals volume than the corresponding mutant residue. The order is defined as %s so some cases are considered to have no change in volume e.g. MET -> LEU.' % (' < '.join([''.join(sorted(v)) for k, v in sorted(volume_groups.iteritems())]))
          ) )
          for subcase in ('XX', 'SL', 'LS'):
              subcase_dataframe = dataframe[dataframe['VolumeChange'] == subcase]
              table_header = 'Statistics - %s (%d cases)' % (BenchmarkRun.by_volume_descriptions[subcase], len(subcase_dataframe))
              if len(subcase_dataframe) >= 8:
                  list_stats = format_stats(get_xy_dataset_statistics_pandas(subcase_dataframe, experimental_field, 'Predicted', fcorrect_x_cutoff = self.stability_classication_x_cutoff, fcorrect_y_cutoff = self.stability_classication_y_cutoff, ignore_null_values = True, run_standardized_analysis = False), return_string = False)
                  section_latex_objs.append( lr.LatexTable(
                      header_row,
                      list_stats,
                      column_format = stats_column_format,
                      header_text = table_header
                  ))
                  self.add_stored_metric_to_df(BenchmarkRun.by_volume_descriptions[subcase], len(subcase_dataframe), list_stats)
              else:
                  section_latex_objs.append( lr.LatexText(
                      'Not enough data for analysis of mutations ''%s'' (at least 8 cases are required).' % BenchmarkRun.by_volume_descriptions[subcase]
                  ))
          if verbose:
              self.report('\n'.join([x.generate_plaintext() for x in section_latex_objs]), fn = colortext.sprint)
          self.metric_latex_objects.extend( section_latex_objs )
  
  
          section_latex_objs = []
          section_latex_objs.append( lr.LatexSubSection(
              'Mutations to alanine',
              'And mutations not to alanine'
          ))
          subcase_dataframe = dataframe[dataframe['MutantAA'] == 'A']
          if len(subcase_dataframe) > 0:
              table_header = 'Statistics - all mutations to alanine (including multiple mutations, if they are all to alanine) (%d cases)' % len(subcase_dataframe)
              list_stats = format_stats(get_xy_dataset_statistics_pandas(subcase_dataframe, experimental_field, 'Predicted', fcorrect_x_cutoff = self.stability_classication_x_cutoff, fcorrect_y_cutoff = self.stability_classication_y_cutoff, ignore_null_values = True, run_standardized_analysis = False), return_string = False)
              section_latex_objs.append( lr.LatexTable(
                  header_row,
                  list_stats,
                  column_format = stats_column_format,
                  header_text = table_header
              ))
              self.add_stored_metric_to_df('all mutations to alanine', len(subcase_dataframe), list_stats)
  
          subcase_dataframe = dataframe[(dataframe['MutantAA'] == 'A') & (dataframe['NumberOfMutations'] == 1)]
          if len(subcase_dataframe) > 0:
              table_header = 'Statistics - single mutations to alanine (%d cases)' % len(subcase_dataframe)
              list_stats = format_stats(get_xy_dataset_statistics_pandas(subcase_dataframe, experimental_field, 'Predicted', fcorrect_x_cutoff = self.stability_classication_x_cutoff, fcorrect_y_cutoff = self.stability_classication_y_cutoff, ignore_null_values = True, run_standardized_analysis = False), return_string = False)
              section_latex_objs.append( lr.LatexTable(
                  header_row,
                  list_stats,
                  column_format = stats_column_format,
                  header_text = table_header
              ))
              self.add_stored_metric_to_df('single mutations to alanine', len(subcase_dataframe), list_stats)
  
          subcase_dataframe = dataframe[(dataframe['MutantAA'] == 'A') & (dataframe['NumberOfMutations'] != 1)]
          if len(subcase_dataframe) > 0:
              table_header = 'Statistics - multiple mutations to alanine (%d cases)' % len(subcase_dataframe)
              list_stats = format_stats(get_xy_dataset_statistics_pandas(subcase_dataframe, experimental_field, 'Predicted', fcorrect_x_cutoff = self.stability_classication_x_cutoff, fcorrect_y_cutoff = self.stability_classication_y_cutoff, ignore_null_values = True, run_standardized_analysis = False), return_string = False)
              section_latex_objs.append( lr.LatexTable(
                  header_row,
                  list_stats,
                  column_format = stats_column_format,
                  header_text = table_header
              ))
              self.add_stored_metric_to_df('multiple mutations to alanine', len(subcase_dataframe), list_stats)
  
          subcase_dataframe = dataframe[dataframe['MutantAA'] != 'A']
          if len(subcase_dataframe) > 0:
              table_header = 'Statistics - mutations to anything other than alanine (including multiple mutations that include a non-alanine mutation) (%d cases)' % len(subcase_dataframe)
              list_stats = format_stats(get_xy_dataset_statistics_pandas(subcase_dataframe, experimental_field, 'Predicted', fcorrect_x_cutoff = self.stability_classication_x_cutoff, fcorrect_y_cutoff = self.stability_classication_y_cutoff, ignore_null_values = True, run_standardized_analysis = False), return_string = False)
              section_latex_objs.append( lr.LatexTable(
                  header_row,
                  list_stats,
                  column_format = stats_column_format,
                  header_text = table_header
              ))
              self.add_stored_metric_to_df('mutations not to alanine', len(subcase_dataframe), list_stats)
          if verbose and len(section_latex_objs) > 0:
              self.report('\n'.join([x.generate_plaintext() for x in section_latex_objs]), fn = colortext.sprint)
          self.metric_latex_objects.extend( section_latex_objs )
  
  
          section_latex_objs = []
          subcase_dataframe = dataframe[dataframe['HasGPMutation'] == 1]
          if len(subcase_dataframe) > 0:
              table_header = 'Statistics - cases with G or P (%d cases)' % len(subcase_dataframe)
              list_stats = format_stats(get_xy_dataset_statistics_pandas(subcase_dataframe, experimental_field, 'Predicted', fcorrect_x_cutoff = self.stability_classication_x_cutoff, fcorrect_y_cutoff = self.stability_classication_y_cutoff, ignore_null_values = True, run_standardized_analysis = False), return_string = False)
              section_latex_objs.append( lr.LatexTable(
                  header_row,
                  list_stats,
                  column_format = stats_column_format,
                  header_text = table_header
              ))
              self.add_stored_metric_to_df('cases with G or P', len(subcase_dataframe), list_stats)
  
          subcase_dataframe = dataframe[dataframe['HasGPMutation'] == 0]
          if len(subcase_dataframe) > 0:
              table_header = 'Statistics - cases without G or P (%d cases)' % len(subcase_dataframe)
              list_stats = format_stats(get_xy_dataset_statistics_pandas(subcase_dataframe, experimental_field, 'Predicted', fcorrect_x_cutoff = self.stability_classication_x_cutoff, fcorrect_y_cutoff = self.stability_classication_y_cutoff, ignore_null_values = True, run_standardized_analysis = False), return_string = False)
              section_latex_objs.append( lr.LatexTable(
                  header_row,
                  list_stats,
                  column_format = stats_column_format,
                  header_text = table_header
              ))
              self.add_stored_metric_to_df('cases without G or P', len(subcase_dataframe), list_stats)
  
          if len(section_latex_objs) > 0:
              section_latex_objs.insert( 0, lr.LatexSubSection(
                  'Separating out mutations involving glycine or proline.',
                  'This cases may involve changes to secondary structure so we separate them out here.'
              ))
  
          if verbose and len(section_latex_objs) > 0:
              self.report('\n'.join([x.generate_plaintext() for x in section_latex_objs]), fn = colortext.sprint)
          self.metric_latex_objects.extend( section_latex_objs )
  
          #### Single mutations
          section_latex_objs = []
          section_latex_objs.append( lr.LatexSubSection(
              'Number of mutations',
          ))
          subcase_dataframe = dataframe[dataframe['NumberOfMutations'] == 1]
          if len(subcase_dataframe) >= case_n_cutoff:
              table_header = 'Statistics - single mutations (%d cases)' % len(subcase_dataframe)
              list_stats = format_stats(get_xy_dataset_statistics_pandas(subcase_dataframe, experimental_field, 'Predicted', fcorrect_x_cutoff = self.stability_classication_x_cutoff, fcorrect_y_cutoff = self.stability_classication_x_cutoff, ignore_null_values = True, run_standardized_analysis = False), return_string = False)
              section_latex_objs.append( lr.LatexTable(
                  header_row,
                  list_stats,
                  column_format = stats_column_format,
                  header_text = table_header
              ))
              self.add_stored_metric_to_df('single mutations', len(subcase_dataframe), list_stats)
          subcase_dataframe = dataframe[dataframe['NumberOfMutations'] > 1]
          if len(subcase_dataframe) >= case_n_cutoff:
              table_header = 'Statistics - multiple mutations (%d cases)' % len(subcase_dataframe)
              list_stats = format_stats(get_xy_dataset_statistics_pandas(subcase_dataframe, experimental_field, 'Predicted', fcorrect_x_cutoff = self.stability_classication_x_cutoff, fcorrect_y_cutoff = self.stability_classication_x_cutoff, ignore_null_values = True, run_standardized_analysis = False), return_string = False)
              section_latex_objs.append( lr.LatexTable(
                  header_row,
                  list_stats,
                  column_format = stats_column_format,
                  header_text = table_header
              ))
              self.add_stored_metric_to_df('multiple mutations', len(subcase_dataframe), list_stats)
          # subcase_dataframe = dataframe[(dataframe.NumberOfMutations >= 2) & (dataframe.NumberOfMutations <= 5)]
          # if len(subcase_dataframe) >= case_n_cutoff:
          #     table_header = 'Statistics - 2-4 mutations (%d cases)' % len(subcase_dataframe)
          #     list_stats = format_stats(get_xy_dataset_statistics_pandas(subcase_dataframe, experimental_field, 'Predicted', fcorrect_x_cutoff = self.stability_classication_x_cutoff, fcorrect_y_cutoff = self.stability_classication_x_cutoff, ignore_null_values = True, run_standardized_analysis = False), return_string = False)
          #     section_latex_objs.append( LatexTable(
          #         header_row,
          #         list_stats,
          #         column_format = stats_column_format,
          #         header_text = table_header
          #     ))
          #     self.add_stored_metric_to_df('2-4 mutations', len(subcase_dataframe), list_stats)
          # mutation_cutoffs = [5, 10, 20, 50, 100, 200]
          # for i, mutation_cutoff in enumerate(mutation_cutoffs):
          #     if len(mutation_cutoffs) - 1 == i:
          #         break
          #     next_cutoff = mutation_cutoffs[i+1]
          #     subcase_dataframe = dataframe[(dataframe.NumberOfMutations >= mutation_cutoff) & (dataframe.NumberOfMutations <= next_cutoff)]
          #     if len(subcase_dataframe) >= case_n_cutoff:
          #         table_header = 'Statistics - %d $<=$ number of mutations $<=$ %d (%d cases)' % (mutation_cutoff, next_cutoff, len(subcase_dataframe))
          #         list_stats = format_stats(get_xy_dataset_statistics_pandas(subcase_dataframe, experimental_field, 'Predicted', fcorrect_x_cutoff = self.stability_classication_x_cutoff, fcorrect_y_cutoff = self.stability_classication_x_cutoff, ignore_null_values = True, run_standardized_analysis = False), return_string = False)
          #         section_latex_objs.append( LatexTable(
          #             header_row,
          #             list_stats,
          #             column_format = stats_column_format,
          #             header_text = table_header
          #         ))
          #         self.add_stored_metric_to_df('%d <= mutations<= %d' % (mutation_cutoff, next_cutoff), len(subcase_dataframe), list_stats)
          if verbose:
              self.report('\n'.join([x.generate_plaintext() for x in section_latex_objs]), fn = colortext.sprint)
          self.metric_latex_objects.extend( section_latex_objs )
          ####
  
          #### Complete dataset (scaled)
          if self.calculate_scalar_adjustments:
              section_latex_objs = []
              section_latex_objs.append( lr.LatexSubSection(
                  'Entire dataset using a scaling factor of 1/%.03f to improve the fraction correct metric.' % scalar_adjustment,
                  'Warning: Results in this section use an averaged scaling factor to improve the value for the fraction correct metric. This scalar will vary over benchmark runs so these results should not be interpreted as performance results; they should be considered as what could be obtained if the predicted values were scaled by a "magic" value.'
              ))
              table_header = 'Statistics - complete dataset (scaled) (%d cases)' % len(dataframe)
              # For these statistics, we assume that we have reduced any scaling issues and use the same cutoff for the Y-axis as the user specified for the X-axis
              list_stats = format_stats(get_xy_dataset_statistics_pandas(dataframe, experimental_field, BenchmarkRun.get_analysis_set_fieldname('Predicted_adj', analysis_set), fcorrect_x_cutoff = self.stability_classication_x_cutoff, fcorrect_y_cutoff = self.stability_classication_x_cutoff, ignore_null_values = True, run_standardized_analysis = False), return_string = False)
              section_latex_objs.append( lr.LatexTable(
                  header_row,
                  list_stats,
                  column_format = stats_column_format,
                  header_text = table_header
              ))
              self.add_stored_metric_to_df('complete dataset (scaled)', len(dataframe), list_stats)
              if verbose:
                  self.report('\n'.join([x.generate_plaintext() for x in section_latex_objs]), fn = colortext.sprint)
              self.metric_latex_objects.extend( section_latex_objs )
          ####
  
          section_latex_objs = []
          section_latex_objs.append( lr.LatexSubSection(
              'Entire dataset',
              'Overall statistics'
          ))
          table_header = 'Statistics - complete dataset (%d cases)' % len(dataframe)
          # For these statistics, we assume that we have reduced any scaling issues and use the same cutoff for the Y-axis as the user specified for the X-axis
          list_stats = format_stats(get_xy_dataset_statistics_pandas(dataframe, experimental_field, 'Predicted', fcorrect_x_cutoff = self.stability_classication_x_cutoff, fcorrect_y_cutoff = self.stability_classication_x_cutoff, ignore_null_values = True, run_standardized_analysis = False), return_string = False)
          section_latex_objs.append( lr.LatexTable(
              header_row,
              list_stats,
              column_format = stats_column_format,
              header_text = table_header
          ))
          self.add_stored_metric_to_df('complete dataset', len(dataframe), list_stats)
          if verbose:
              self.report('\n'.join([x.generate_plaintext() for x in section_latex_objs]), fn = colortext.sprint)
          self.metric_latex_objects.extend( section_latex_objs )
  
          # There is probably a better way of writing the pandas code here
          record_with_most_errors = (dataframe[['PDBFileID', 'NumberOfDerivativeErrors', 'Mutations']].sort_values(by = 'NumberOfDerivativeErrors')).tail(1)
          record_index = record_with_most_errors.index.tolist()[0]
          pdb_id, num_errors, mutation_str = dataframe.loc[record_index, 'PDBFileID'], dataframe.loc[record_index, 'NumberOfDerivativeErrors'], dataframe.loc[record_index, 'Mutations']
          if num_errors > 0:
              error_detection_text = '\n\nDerivative errors were found in the run. Record #{0} - {1}, {2} - has the most amount ({3}) of derivative errors.'.format(record_index, pdb_id, mutation_str, num_errors)
              self.metric_latex_objects.append( lr.LatexText(error_detection_text, color = 'red') )
              if verbose:
                  self.report(error_detection_text, fn = colortext.warning)
  
          # Write the analysis to file
          self.create_analysis_directory(analysis_directory)
          self.metrics_filepath = os.path.join(self.analysis_directory, '{0}_metrics.txt'.format(self.benchmark_run_name))
          write_file(self.metrics_filepath, '\n'.join([x.generate_plaintext() for x in self.metric_latex_objects]))