Create a dataframe row for a prediction.

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