Return the best performing ensemble. If the user hasn't specified a FPF, the default behavior sorts ensembles by
    largest enrichment factor at the smallest FPF (1 / n, where n is the total number of decoys). If the user supplied
    a FPF, ensembles are sorted by the largest enrichment factor at the input FPF. If the user supplied a FPF = 1,
    ensembles are sorted by AUC. Ties are broken by considering enrichment factors at the smallest FPF not already
    considered.
    :param results: {ensemble_storage_object1, ensemble_storage_object2, ..., ensemble_storage_objectn}
    :param options: options object, contains user-specified arguments as attributes.
    :return: ensemble_storage_object (classification.EnsembleStorage) that contains the best performing ensemble

def find_best_ensemble(results, options):
    """
    Return the best performing ensemble. If the user hasn't specified a FPF, the default behavior sorts ensembles by
    largest enrichment factor at the smallest FPF (1 / n, where n is the total number of decoys). If the user supplied
    a FPF, ensembles are sorted by the largest enrichment factor at the input FPF. If the user supplied a FPF = 1,
    ensembles are sorted by AUC. Ties are broken by considering enrichment factors at the smallest FPF not already
    considered.
    :param results: {ensemble_storage_object1, ensemble_storage_object2, ..., ensemble_storage_objectn}
    :param options: options object, contains user-specified arguments as attributes.
    :return: ensemble_storage_object (classification.EnsembleStorage) that contains the best performing ensemble
    """

    # We need the total number of decoys in the set to determine the number of decoys that correspond to the FPF values
    # at which enrichment factors were measured. The number of decoys are keys in the ef dictionary of each
    # ensemble object stored in the results dictionary.
    n = sorted(list(list(results.items())[0][1].ef.keys()), reverse=True)[0]

    # determine the number of decoys that correspond to the FPF used for training
    if not options.fpf:
        ndecoys = 1
    else:
        ndecoys = int(round(n * options.fpf))

    # sort the results according to the user-specified training method
    if ndecoys == n:
        # the user specified an fpf of 1, so wants the ensemble the maximizes the AUC, so sort on auc
        prop_key = 'auc'
        sorted_list = sorted(results.items(), key = lambda x: x[1].get_prop(prop_key), reverse=True)
    else:
        # the user is interested in an ensemble that maximizes an enrichment factor at some FPF
        prop_key = 'ef'
        sorted_list = sorted(results.items(), key = lambda x: x[1].get_prop(ndecoys, prop_key), reverse=True)

    # we only need to consider breaking a tie if there is more than one ensemble to consider
    if len(sorted_list) > 1:
        sorted_list = tie_break(sorted_list, results, prop_key, ndecoys)

    return sorted_list[0][0]