docs: def get_dataframe_row(self, dataset_cases, predicted_data, pdb_data, record_id, additional_prediction_data_columns):
          '''Create a dataframe row for a prediction.'''
  
          # Ignore derived mutations if appropriate
          record = dataset_cases[record_id]
          if self.is_this_record_a_derived_mutation(record) and not self.include_derived_mutations:
              return None
  
          amino_acid_details, CAA, PAA, HAA = self.amino_acid_details, self.CAA, self.PAA, self.HAA
          burial_cutoff = self.burial_cutoff
  
          # Initialize variables. For ambiguous cases where the set of distinct values has multiple values, we default to None
          residue_charge, residue_charges = None, set()
          exposure, exposures = None, set()
          volume_change, volume_changes = None, set()
          record_wtaa, wtaas = None, set()
          record_mutaa, mutaas = None, set()
          DSSPSimpleSSType, DSSPSimpleSSTypes = None, set()
          DSSPType, DSSPTypes = None, set()
          DSSPExposure, DSSPExposures = None, set()
          scops = set()
          mutation_string = []
          num_derivative_errors = predicted_data.get('Errors', {}).get('Derivative error count', 0)
          run_time = predicted_data.get('RunTime', None)
          max_memory = predicted_data.get('MaxMemory', None)
  
          mutations = self.get_record_mutations(record)
          for m in mutations:
  
              wtaa = m['WildTypeAA']
              mutaa = m['MutantAA']
              mutation_string.append('{0} {1}{2}{3}'.format(m['Chain'], m['WildTypeAA'], m['ResidueID'], m['MutantAA']))
  
              # Residue types and chain
              wtaas.add(wtaa)
              mutaas.add(mutaa)
              if m.get('SCOP class'):
                  scops.add(m['SCOP class'])
              DSSPSimpleSSTypes.add(m['DSSPSimpleSSType'])
              DSSPTypes.add(m['DSSPType'])
              DSSPExposures.add(m['DSSPExposure'])
  
              # Burial
              if m['DSSPExposure'] != None:
                  if m['DSSPExposure'] > burial_cutoff:
                      exposures.add('E')
                  else:
                      exposures.add('B')
              else:
                  exposures.add(None)
  
              # Volume
              if amino_acid_details[wtaa]['van der Waals volume'] < amino_acid_details[mutaa]['van der Waals volume']:
                  volume_changes.add('SL')
              elif amino_acid_details[wtaa]['van der Waals volume'] > amino_acid_details[mutaa]['van der Waals volume']:
                  volume_changes.add('LS')
              elif amino_acid_details[wtaa]['van der Waals volume'] == amino_acid_details[mutaa]['van der Waals volume']:
                  volume_changes.add('XX')
  
              # Charge
              if ((wtaa in CAA or wtaa in PAA) and (mutaa in HAA)) or ((mutaa in CAA or mutaa in PAA) and (wtaa in HAA)):
                  residue_charges.add('Change')
              elif (wtaa in CAA or wtaa in PAA) and (mutaa in CAA or mutaa in PAA):
                  residue_charges.add('Polar/Charged')
              elif (wtaa in HAA) and (mutaa in HAA):
                  residue_charges.add('Hydrophobic/Non-polar')
              else:
                   raise colortext.Exception('Should not reach here.')
  
          # Create a string representing the mutations (useful for labeling rather than analysis)
          mutation_string = '; '.join(mutation_string)
  
          # Taking unique values, determine the residue charges of the wildtype and mutant residues, the wildtype residue exposure, and the relative change in van der Waals volume
          if len(residue_charges) == 1: residue_charge = residue_charges.pop()
          if len(exposures) == 1: exposure = exposures.pop()
          if len(volume_changes) == 1: volume_change = volume_changes.pop()
  
          # Taking unique values, determine the wildtype and mutant residue types
          all_residues = wtaas.union(mutaas)
          if len(wtaas) == 1: record_wtaa = wtaas.pop()
          if len(mutaas) == 1: record_mutaa = mutaas.pop()
  
          # Taking unique values, determine the secondary structure and residue exposures from the DSSP data in the dataset
          if len(DSSPSimpleSSTypes) == 1: DSSPSimpleSSType = DSSPSimpleSSTypes.pop()
          if len(DSSPTypes) == 1: DSSPType = DSSPTypes.pop()
          if len(DSSPExposures) == 1: DSSPExposure = DSSPExposures.pop()
  
          # Determine the SCOP classification from the SCOPe data in the dataset
          full_scop_classification, scop_class, scop_fold = None, None, None
          if len(scops) > 1:
              self.log('Warning: There is more than one SCOPe class for record {0}.'.format(record_id), colortext.warning)
          elif len(scops) == 1:
              full_scop_classification = scops.pop()
              scop_tokens = full_scop_classification.split('.')
              scop_class = scop_tokens[0]
              if len(scop_tokens) > 1:
                  scop_fold = '.'.join(scop_tokens[0:2])
  
          # Partition the data by PDB resolution with bins: N/A, <1.5, 1.5-<2.0, 2.0-<2.5, >=2.5
          pdb_record = pdb_data.get(self.get_record_pdb_file_id(record).upper())
          pdb_resolution_bin = None
          pdb_resolution = pdb_record.get('Resolution')
          if pdb_resolution != None:
              if pdb_resolution < 1.5:
                  pdb_resolution_bin = '<1.5'
              elif pdb_resolution < 2.0:
                  pdb_resolution_bin = '1.5-2.0'
              elif pdb_resolution < 2.5:
                  pdb_resolution_bin = '2.0-2.5'
              else:
                  pdb_resolution_bin = '>=2.5'
          pdb_resolution_bin = pdb_resolution_bin or 'N/A'
  
          # Mark mutations involving glycine or proline
          has_gp_mutation = 'G' in all_residues or 'P' in all_residues
  
          # Create the data matrix
          dataframe_record = dict(
              DatasetID = record_id,
              PDBFileID = self.get_record_pdb_file_id(record),
              Mutations = mutation_string,
              NumberOfMutations = len(mutations),
              Predicted = predicted_data[self.ddg_analysis_type],
              ResidueCharges = residue_charge,
              VolumeChange = volume_change,
              HasGPMutation = int(has_gp_mutation),
              WildTypeDSSPType = DSSPType,
              WildTypeDSSPSimpleSSType = DSSPSimpleSSType,
              WildTypeDSSPExposure = DSSPExposure,
              WildTypeSCOPClass = scop_class,
              WildTypeSCOPFold = scop_fold,
              WildTypeSCOPClassification = full_scop_classification,
              WildTypeExposure = exposure,
              WildTypeAA = record_wtaa,
              MutantAA = record_mutaa,
              PDBResolution = pdb_record.get('Resolution'),
              PDBResolutionBin = pdb_resolution_bin,
              NumberOfResidues = self.count_residues(record, pdb_record) or None,
              NumberOfDerivativeErrors = num_derivative_errors,
              RunTime = run_time,
              MaxMemory = max_memory,
              )
          for c in additional_prediction_data_columns:
              dataframe_record[c] = predicted_data.get(c)
  
          if self.contains_experimental_data:
              # These fields are particular to dataframes containing experimental values e.g. for benchmarking runs or for
              # datasets where we have associated experimental values
              self.get_experimental_ddg_values(record, dataframe_record)
              self.compute_stability_classification(predicted_data, record, dataframe_record)
              self.compute_absolute_error(predicted_data, record, dataframe_record)
  
          return dataframe_record