Source code for rubin_sim.maf.run_comparison.radar_plot

__all__ = (
    "radar",
    "normalize_for_radar",
)

import matplotlib.pylab as plt
import numpy as np
from matplotlib.path import Path
from matplotlib.projections import register_projection
from matplotlib.projections.polar import PolarAxes
from matplotlib.spines import Spine

# Starting with example at
# https://matplotlib.org/examples/api/radar_chart.html


[docs] def normalize_for_radar( summary, norm_run="baseline", invert_cols=None, reverse_cols=None, mag_cols=[], ): """Normalize values in a dataframe to a given run, return output in a dataframe. This provides a similar functionality as the normalize_metric_summaries method, and returns a similar dataframe. The options for specifying which columns to invert, reverse, or identify as 'magnitudes' are slightly different, instead of using a 'metric_set'. Parameters ---------- summary : `pandas.DataFrame` The data frame containing the metric summary stats to normalize (such as from `get_metric_summaries`). Note that this should contain only the runs and metrics to be normalized -- e.g. `summary.loc[[list of runs], [list of metrics]]` summary should be indexed by the run name. norm_run : `str` The name of the run to use to define the normalization. invert_cols : `list` [`str`] A list of column names that should be inverted (e.g., columns that are uncertainties and are better with a smaller value) reverse_cols : `list` [`str] Columns to reverse (e.g., magnitudes) mag_cols : `list` [`str`] Columns that are in magnitudes """ out_df = summary.copy() if reverse_cols is not None: for colname in reverse_cols: out_df[colname] = -out_df[colname] if invert_cols is not None: for colname in invert_cols: out_df[colname] = 1.0 / out_df[colname] if norm_run is not None: indx = np.max(np.where(out_df.index == norm_run)[0]) for col in out_df.columns: if col != "run_name": if (col in mag_cols) | (mag_cols == "all"): out_df[col] = 1.0 + (out_df[col] - out_df[col].iloc[indx]) else: out_df[col] = 1.0 + (out_df[col] - out_df[col].iloc[indx]) / out_df[col].iloc[indx] return out_df
def _radar_factory(num_vars, frame="circle"): """Create a radar chart with `num_vars` axes. This function creates a RadarAxes projection and registers it. Parameters ---------- num_vars : `int` Number of variables for radar chart. frame : {'circle' | 'polygon'} Shape of frame surrounding axes. """ # calculate evenly-spaced axis angles theta = np.linspace(0, 2 * np.pi, num_vars, endpoint=False) # rotate theta such that the first axis is at the top, # then make sure we don't go past 360 theta += np.pi / 2 theta = theta % (2.0 * np.pi) def draw_poly_patch(self): verts = _unit_poly_verts(theta) return plt.Polygon(verts, closed=True, edgecolor="k") def draw_circle_patch(self): # unit circle centered on (0.5, 0.5) return plt.Circle((0.5, 0.5), 0.5) patch_dict = {"polygon": draw_poly_patch, "circle": draw_circle_patch} if frame not in patch_dict: raise ValueError("unknown value for `frame`: %s" % frame) class RadarAxes(PolarAxes): name = "radar" # use 1 line segment to connect specified points RESOLUTION = 1 # define draw_frame method draw_patch = patch_dict[frame] def fill(self, *args, **kwargs): """Override fill so that line is closed by default""" closed = kwargs.pop("closed", True) return super(RadarAxes, self).fill(closed=closed, *args, **kwargs) def plot(self, *args, **kwargs): """Override plot so that line is closed by default""" lines = super(RadarAxes, self).plot(*args, **kwargs) for line in lines: self._close_line(line) def _close_line(self, line): x, y = line.get_data() # FIXME: markers at x[0], y[0] get doubled-up if x[0] != x[-1]: x = np.concatenate((x, [x[0]])) y = np.concatenate((y, [y[0]])) line.set_data(x, y) def set_varlabels(self, labels): self.set_thetagrids(np.degrees(theta), labels) def _gen_axes_patch(self): return self.draw_patch() def _gen_axes_spines(self): if frame == "circle": return PolarAxes._gen_axes_spines(self) # The following is a hack to get the spines (i.e. the axes frame) # to draw correctly for a polygon frame. # spine_type must be 'left', 'right', 'top', 'bottom', or `circle`. spine_type = "circle" verts = _unit_poly_verts(theta) # close off polygon by repeating first vertex verts.append(verts[0]) path = Path(verts) spine = Spine(self, spine_type, path) spine.set_transform(self.transAxes) return {"polar": spine} register_projection(RadarAxes) return theta def _unit_poly_verts(theta): """Return vertices of polygon for subplot axes. This polygon is circumscribed by a unit circle centered at (0.5, 0.5) """ x0, y0, r = [0.5] * 3 verts = [(r * np.cos(t) + x0, r * np.sin(t) + y0) for t in theta] return verts
[docs] def radar( df, rgrids=[0.7, 1.0, 1.3, 1.6], colors=None, alpha=0.1, legend=True, figsize=(8.5, 5), fill=False, bbox_to_anchor=(1.6, 0.5), legend_font_size=None, ): """make a radar plot!""" theta = _radar_factory(np.size(df.columns), frame="polygon") fig, axes = plt.subplots(figsize=figsize, subplot_kw=dict(projection="radar")) axes.set_rgrids(rgrids, fontsize="x-large") if colors is None: colors = [None for i in range(len(df))] ix = 0 for i, row in df.iterrows(): axes.plot(theta, row.values, "o-", label=i, color=colors[ix]) if fill: axes.fill(theta, row.values, alpha=alpha) ix += 1 variables = df.columns.values axes.set_varlabels(variables) if legend: axes.legend( bbox_to_anchor=bbox_to_anchor, borderaxespad=0, loc="lower right", fontsize=legend_font_size, ) axes.set_ylim([np.min(rgrids), np.max(rgrids)]) return fig, axes