Projects the grid search results onto 2 dimensions.

    The display value is taken as the max over the non-displayed dimensions.

    Parameters
    ----------
    cv_results : dict
        A dictionary of results from the `GridSearchCV` object's `cv_results_`
        attribute.

    x_param : string
        The name of the parameter to be visualized on the horizontal axis.

    y_param : string
        The name of the parameter to be visualized on the vertical axis.

    metric : string (default 'mean_test_score')
        The field from the grid search's `cv_results` that we want to display.

    Returns
    -------
    unique_x_vals : list
        The parameter values that will be used to label the x axis.

    unique_y_vals: list
        The parameter values that will be used to label the y axis.

    best_scores: 2D numpy array (n_y by n_x)
        Array of scores to be displayed for each parameter value pair.

def param_projection(cv_results, x_param, y_param, metric='mean_test_score'):
    """
    Projects the grid search results onto 2 dimensions.

    The display value is taken as the max over the non-displayed dimensions.

    Parameters
    ----------
    cv_results : dict
        A dictionary of results from the `GridSearchCV` object's `cv_results_`
        attribute.

    x_param : string
        The name of the parameter to be visualized on the horizontal axis.

    y_param : string
        The name of the parameter to be visualized on the vertical axis.

    metric : string (default 'mean_test_score')
        The field from the grid search's `cv_results` that we want to display.

    Returns
    -------
    unique_x_vals : list
        The parameter values that will be used to label the x axis.

    unique_y_vals: list
        The parameter values that will be used to label the y axis.

    best_scores: 2D numpy array (n_y by n_x)
        Array of scores to be displayed for each parameter value pair.
    """
    # Extract the parameter values and score corresponding to each gridsearch
    # trial.
    # These are masked arrays where the cases where each parameter is
    # non-applicable are masked.
    try:
        x_vals = cv_results['param_' + x_param]
    except KeyError:
        raise YellowbrickKeyError("Parameter '{}' does not exist in the grid "
                                  "search results".format(x_param))
    try:
        y_vals = cv_results['param_' + y_param]
    except KeyError:
        raise YellowbrickKeyError("Parameter '{}' does not exist in the grid "
                                  "search results".format(y_param))

    if metric not in cv_results:
        raise YellowbrickKeyError("Metric '{}' does not exist in the grid "
                                  "search results".format(metric))

    # Get unique, unmasked values of the two display parameters
    unique_x_vals = sorted(list(set(x_vals.compressed())))
    unique_y_vals = sorted(list(set(y_vals.compressed())))
    n_x = len(unique_x_vals)
    n_y = len(unique_y_vals)

    # Get mapping of each parameter value -> an integer index
    int_mapping_1 = {value: idx for idx, value in enumerate(unique_x_vals)}
    int_mapping_2 = {value: idx for idx, value in enumerate(unique_y_vals)}

    # Translate each gridsearch result to indices on the grid
    idx_x = [int_mapping_1[value] if value else None for value in x_vals]
    idx_y = [int_mapping_2[value] if value else None for value in y_vals]

    # Create an array of all scores for each value of the display parameters.
    # This is a n_x by n_y array of lists with `None` in place of empties
    # (my kingdom for a dataframe...)
    all_scores = [[None for _ in range(n_x)] for _ in range(n_y)]
    for x, y, score in zip(idx_x, idx_y, cv_results[metric]):
        if x is not None and y is not None:
            if all_scores[y][x] is None:
                all_scores[y][x] = []
            all_scores[y][x].append(score)

    # Get a numpy array consisting of the best scores for each parameter pair
    best_scores = np.empty((n_y, n_x))
    for x in range(n_x):
        for y in range(n_y):
            if all_scores[y][x] is None:
                best_scores[y, x] = np.nan
            else:
                try:
                    best_scores[y, x] = max(all_scores[y][x])
                except ValueError:
                    raise YellowbrickValueError(
                        "Cannot display grid search results for metric '{}': "
                        "result values may not all be numeric".format(metric)
                    )

    return unique_x_vals, unique_y_vals, best_scores