def constrain_column_types(table, col_type_dict=None, nan_value_dict=None,
    replace_value_dict=None):
    """
    Helper method that helps in constraining data types for the
    various DataFrame columns.

    This function checks a table's column types against a defined
    column name/type dictionary and correct them if necessary.

    .. notes::

    There are fewer pandas `dtypes` than the corresponding numpy
        type, but all numpy types can be accommodated.

    NaNs and Upcasting:
    The upcasting of dtypes on columns that contain NaNs has been an issue and
        can lead to inconsistencies between the column types of different
        tables that contain equivalent data. E.g., a `merged_table` from a PDB
        entry may have NaNs in UniProt_dbResNum and so is cast to float64
        whilst a UniProt variants table will have no NaNs in this field and
        so remains int64 by default. The main issue here is that it creates
        additional work for merging these tables as the keys must be of the
        same type. It's also a problem if you want to test elements say for
        example you expect to be testing integer equality but the values have
        been coerced to floats.

    Ideally then we need to ensure that all equivalent column types are by
        default the least generic dtype that can contain all possible values
        that can be seen in the column, i.e. if NaNs are possible then even
        when not present, a column of integers that can contain NaNs should
        always be at least float.

    dtypes and element types:
    It seems that coercion to certain dtypes alters the element types too.
        For instance, int64 -> float64 will make an equivalent change to
        individual value types. However, the original element types can be
        preserved with the generic `object` dtype, so this will be used in
        preference for integer columns that can contain NaNs.

    :param table: pandas DataFrame
    :param col_type_dict: (dict) optional defines common types
    :param nan_value_dict: (dict) optional new value passed to replace NaNs
    :param replace_value_dict: (dict) optional new value passed to replace
        specific values
    :return: modified pandas DataFrame
        (the table with the same data, but normalised column types)
    """
    for col in table:
        if col_type_dict is not None and col in col_type_dict:
            try:
                table[col] = table[col].astype(col_type_dict[col])
            except (ValueError, KeyError, TypeError):
                pass
        if nan_value_dict is not None and col in nan_value_dict:
            if table[col].isnull().any().any():
                table[col] = table[col].fillna(nan_value_dict[col])
        if replace_value_dict is not None and col in replace_value_dict:
            table[col] = table[col].replace(replace_value_dict[col][0],
                replace_value_dict[col][1])
    return table
