Skip to content
Snippets Groups Projects
Select Git revision
  • 03eaf0ad9cdaea9f57de20e334514c1847940064
  • master default protected
  • use_flight_altitude
  • distribute
4 results

performance_diagrams.py

Blame
  • user avatar
    tomrink authored
    cd125b59
    History
    performance_diagrams.py 11.79 KiB
    """Methods for plotting performance diagram."""
    
    import numpy
    import matplotlib.colors
    import matplotlib.pyplot as pyplot
    
    DEFAULT_LINE_COLOUR = numpy.array([228, 26, 28], dtype=float) / 255
    DEFAULT_LINE_WIDTH = 3
    DEFAULT_BIAS_LINE_COLOUR = numpy.full(3, 152. / 255)
    DEFAULT_BIAS_LINE_WIDTH = 2
    
    LEVELS_FOR_CSI_CONTOURS = numpy.linspace(0, 1, num=11, dtype=float)
    LEVELS_FOR_BIAS_CONTOURS = numpy.array(
        [0.25, 0.5, 0.75, 1., 1.5, 2., 3., 5.])
    
    BIAS_STRING_FORMAT = '%.2f'
    BIAS_LABEL_PADDING_PX = 10
    
    FIGURE_WIDTH_INCHES = 10
    FIGURE_HEIGHT_INCHES = 10
    
    FONT_SIZE = 20
    pyplot.rc('font', size=FONT_SIZE)
    pyplot.rc('axes', titlesize=FONT_SIZE)
    pyplot.rc('axes', labelsize=FONT_SIZE)
    pyplot.rc('xtick', labelsize=FONT_SIZE)
    pyplot.rc('ytick', labelsize=FONT_SIZE)
    pyplot.rc('legend', fontsize=FONT_SIZE)
    pyplot.rc('figure', titlesize=FONT_SIZE)
    
    
    def _get_sr_pod_grid(success_ratio_spacing=0.01, pod_spacing=0.01):
        """Creates grid in SR-POD (success ratio / probability of detection) space.
        M = number of rows (unique POD values) in grid
        N = number of columns (unique success ratios) in grid
        :param success_ratio_spacing: Spacing between grid cells in adjacent
            columns.
        :param pod_spacing: Spacing between grid cells in adjacent rows.
        :return: success_ratio_matrix: M-by-N numpy array of success ratios.
            Success ratio increases with column index.
        :return: pod_matrix: M-by-N numpy array of POD values.  POD decreases with
            row index.
        """
    
        num_success_ratios = 1 + int(numpy.ceil(1. / success_ratio_spacing))
        num_pod_values = 1 + int(numpy.ceil(1. / pod_spacing))
    
        unique_success_ratios = numpy.linspace(0., 1., num=num_success_ratios)
        unique_pod_values = numpy.linspace(0., 1., num=num_pod_values)[::-1]
        return numpy.meshgrid(unique_success_ratios, unique_pod_values)
    
    
    def _csi_from_sr_and_pod(success_ratio_array, pod_array):
        """Computes CSI (critical success index) from success ratio and POD.
        POD = probability of detection
        :param success_ratio_array: numpy array (any shape) of success ratios.
        :param pod_array: numpy array (same shape) of POD values.
        :return: csi_array: numpy array (same shape) of CSI values.
        """
    
        return (success_ratio_array ** -1 + pod_array ** -1 - 1.) ** -1
    
    
    def _bias_from_sr_and_pod(success_ratio_array, pod_array):
        """Computes frequency bias from success ratio and POD.
        POD = probability of detection
        :param success_ratio_array: numpy array (any shape) of success ratios.
        :param pod_array: numpy array (same shape) of POD values.
        :return: frequency_bias_array: numpy array (same shape) of frequency biases.
        """
    
        return pod_array / success_ratio_array
    
    
    def _get_csi_colour_scheme():
        """Returns colour scheme for CSI (critical success index).
        :return: colour_map_object: Colour scheme (instance of
            `matplotlib.colors.ListedColormap`).
        :return: colour_norm_object: Instance of `matplotlib.colors.BoundaryNorm`,
            defining the scale of the colour map.
        """
    
        this_colour_map_object = pyplot.cm.Blues
        this_colour_norm_object = matplotlib.colors.BoundaryNorm(
            LEVELS_FOR_CSI_CONTOURS, this_colour_map_object.N)
    
        rgba_matrix = this_colour_map_object(this_colour_norm_object(
            LEVELS_FOR_CSI_CONTOURS))
        colour_list = [
            rgba_matrix[i, ..., :-1] for i in range(rgba_matrix.shape[0])
        ]
    
        colour_map_object = matplotlib.colors.ListedColormap(colour_list)
        colour_map_object.set_under(numpy.array([1, 1, 1]))
        colour_norm_object = matplotlib.colors.BoundaryNorm(
            LEVELS_FOR_CSI_CONTOURS, colour_map_object.N)
    
        return colour_map_object, colour_norm_object
    
    
    def _add_colour_bar(
            axes_object, colour_map_object, values_to_colour, min_colour_value,
            max_colour_value, colour_norm_object=None,
            orientation_string='vertical', extend_min=True, extend_max=True,
            fraction_of_axis_length=1., font_size=FONT_SIZE):
        """Adds colour bar to existing axes.
        :param axes_object: Existing axes (instance of
            `matplotlib.axes._subplots.AxesSubplot`).
        :param colour_map_object: Colour scheme (instance of
            `matplotlib.pyplot.cm`).
        :param values_to_colour: numpy array of values to colour.
        :param min_colour_value: Minimum value in colour map.
        :param max_colour_value: Max value in colour map.
        :param colour_norm_object: Instance of `matplotlib.colors.BoundaryNorm`,
            defining the scale of the colour map.  If `colour_norm_object is None`,
            will assume that scale is linear.
        :param orientation_string: Orientation of colour bar ("vertical" or
            "horizontal").
        :param extend_min: Boolean flag.  If True, the bottom of the colour bar will
            have an arrow.  If False, it will be a flat line, suggesting that lower
            values are not possible.
        :param extend_max: Same but for top of colour bar.
        :param fraction_of_axis_length: Fraction of axis length (y-axis if
            orientation is "vertical", x-axis if orientation is "horizontal")
            occupied by colour bar.
        :param font_size: Font size for labels on colour bar.
        :return: colour_bar_object: Colour bar (instance of
            `matplotlib.pyplot.colorbar`) created by this method.
        """
    
        if colour_norm_object is None:
            colour_norm_object = matplotlib.colors.Normalize(
                vmin=min_colour_value, vmax=max_colour_value, clip=False)
    
        scalar_mappable_object = pyplot.cm.ScalarMappable(
            cmap=colour_map_object, norm=colour_norm_object)
        scalar_mappable_object.set_array(values_to_colour)
    
        if extend_min and extend_max:
            extend_string = 'both'
        elif extend_min:
            extend_string = 'min'
        elif extend_max:
            extend_string = 'max'
        else:
            extend_string = 'neither'
    
        if orientation_string == 'horizontal':
            padding = 0.075
        else:
            padding = 0.05
    
        colour_bar_object = pyplot.colorbar(
            ax=axes_object, mappable=scalar_mappable_object,
            orientation=orientation_string, pad=padding, extend=extend_string,
            shrink=fraction_of_axis_length)
    
        colour_bar_object.ax.tick_params(labelsize=font_size)
        return colour_bar_object
    
    
    def get_points_in_perf_diagram(observed_labels, forecast_probabilities):
        """Creates points for performance diagram.
        E = number of examples
        T = number of binarization thresholds
        :param observed_labels: length-E numpy array of class labels (integers in
            0...1).
        :param forecast_probabilities: length-E numpy array with forecast
            probabilities of label = 1.
        :return: pod_by_threshold: length-T numpy array of POD (probability of
            detection) values.
        :return: success_ratio_by_threshold: length-T numpy array of success ratios.
        """
    
        assert numpy.all(numpy.logical_or(
            observed_labels == 0, observed_labels == 1
        ))
    
        assert numpy.all(numpy.logical_and(
            forecast_probabilities >= 0, forecast_probabilities <= 1
        ))
    
        observed_labels = observed_labels.astype(int)
        binarization_thresholds = numpy.linspace(0, 1, num=1001, dtype=float)
    
        num_thresholds = len(binarization_thresholds)
        pod_by_threshold = numpy.full(num_thresholds, numpy.nan)
        success_ratio_by_threshold = numpy.full(num_thresholds, numpy.nan)
    
        for k in range(num_thresholds):
            these_forecast_labels = (
                forecast_probabilities >= binarization_thresholds[k]
            ).astype(int)
    
            this_num_hits = numpy.sum(numpy.logical_and(
                these_forecast_labels == 1, observed_labels == 1
            ))
    
            this_num_false_alarms = numpy.sum(numpy.logical_and(
                these_forecast_labels == 1, observed_labels == 0
            ))
    
            this_num_misses = numpy.sum(numpy.logical_and(
                these_forecast_labels == 0, observed_labels == 1
            ))
    
            try:
                pod_by_threshold[k] = (
                    float(this_num_hits) / (this_num_hits + this_num_misses)
                )
            except ZeroDivisionError:
                pass
    
            try:
                success_ratio_by_threshold[k] = (
                    float(this_num_hits) / (this_num_hits + this_num_false_alarms)
                )
            except ZeroDivisionError:
                pass
    
        pod_by_threshold = numpy.array([1.] + pod_by_threshold.tolist() + [0.])
        success_ratio_by_threshold = numpy.array(
            [0.] + success_ratio_by_threshold.tolist() + [1.]
        )
    
        return pod_by_threshold, success_ratio_by_threshold
    
    
    def plot_performance_diagram(
            observed_labels, forecast_probabilities,
            line_colour=DEFAULT_LINE_COLOUR, line_width=DEFAULT_LINE_WIDTH,
            bias_line_colour=DEFAULT_BIAS_LINE_COLOUR,
            bias_line_width=DEFAULT_BIAS_LINE_WIDTH, axes_object=None):
        """Plots performance diagram.
        E = number of examples
        :param observed_labels: length-E numpy array of class labels (integers in
            0...1).
        :param forecast_probabilities: length-E numpy array with forecast
            probabilities of label = 1.
        :param line_colour: Colour (in any format accepted by `matplotlib.colors`).
        :param line_width: Line width (real positive number).
        :param bias_line_colour: Colour of contour lines for frequency bias.
        :param bias_line_width: Width of contour lines for frequency bias.
        :param axes_object: Will plot on these axes (instance of
            `matplotlib.axes._subplots.AxesSubplot`).  If `axes_object is None`,
            will create new axes.
        :return: pod_by_threshold: See doc for `get_points_in_perf_diagram`.
            detection) values.
        :return: success_ratio_by_threshold: Same.
        """
    
        pod_by_threshold, success_ratio_by_threshold = get_points_in_perf_diagram(
            observed_labels=observed_labels,
            forecast_probabilities=forecast_probabilities)
    
        if axes_object is None:
            _, axes_object = pyplot.subplots(
                1, 1, figsize=(FIGURE_WIDTH_INCHES, FIGURE_HEIGHT_INCHES)
            )
    
        success_ratio_matrix, pod_matrix = _get_sr_pod_grid()
        csi_matrix = _csi_from_sr_and_pod(success_ratio_matrix, pod_matrix)
        frequency_bias_matrix = _bias_from_sr_and_pod(
            success_ratio_matrix, pod_matrix)
    
        this_colour_map_object, this_colour_norm_object = _get_csi_colour_scheme()
    
        pyplot.contourf(
            success_ratio_matrix, pod_matrix, csi_matrix, LEVELS_FOR_CSI_CONTOURS,
            cmap=this_colour_map_object, norm=this_colour_norm_object, vmin=0.,
            vmax=1., axes=axes_object)
    
        colour_bar_object = _add_colour_bar(
            axes_object=axes_object, colour_map_object=this_colour_map_object,
            colour_norm_object=this_colour_norm_object,
            values_to_colour=csi_matrix, min_colour_value=0.,
            max_colour_value=1., orientation_string='vertical',
            extend_min=False, extend_max=False)
        colour_bar_object.set_label('CSI (critical success index)')
    
        bias_colour_tuple = ()
        for _ in range(len(LEVELS_FOR_BIAS_CONTOURS)):
            bias_colour_tuple += (bias_line_colour,)
    
        bias_contour_object = pyplot.contour(
            success_ratio_matrix, pod_matrix, frequency_bias_matrix,
            LEVELS_FOR_BIAS_CONTOURS, colors=bias_colour_tuple,
            linewidths=bias_line_width, linestyles='dashed', axes=axes_object)
        pyplot.clabel(
            bias_contour_object, inline=True, inline_spacing=BIAS_LABEL_PADDING_PX,
            fmt=BIAS_STRING_FORMAT, fontsize=FONT_SIZE)
    
        nan_flags = numpy.logical_or(
            numpy.isnan(success_ratio_by_threshold), numpy.isnan(pod_by_threshold)
        )
    
        if not numpy.all(nan_flags):
            real_indices = numpy.where(numpy.invert(nan_flags))[0]
            axes_object.plot(
                success_ratio_by_threshold[real_indices],
                pod_by_threshold[real_indices], color=line_colour,
                linestyle='solid', linewidth=line_width)
    
        axes_object.set_xlabel('Success ratio (1 - FAR)')
        axes_object.set_ylabel('POD (probability of detection)')
        axes_object.set_xlim(0., 1.)
        axes_object.set_ylim(0., 1.)
    
        return pod_by_threshold, success_ratio_by_threshold