mgplot.run_plot
run_plot.py This code contains a function to plot and highlighted the 'runs' in a series.
1""" 2run_plot.py 3This code contains a function to plot and highlighted 4the 'runs' in a series. 5""" 6 7# --- imports 8from collections.abc import Sequence 9from typing import Unpack, NotRequired 10from pandas import Series, concat 11from matplotlib.pyplot import Axes 12from matplotlib import patheffects as pe 13 14from mgplot.settings import DataT, get_setting 15from mgplot.axis_utils import map_periodindex, set_labels 16from mgplot.line_plot import line_plot, LineKwargs 17from mgplot.keyword_checking import ( 18 validate_kwargs, 19 report_kwargs, 20 limit_kwargs, 21) 22from mgplot.utilities import constrain_data, check_clean_timeseries 23 24# --- constants 25ME = "run_plot" 26 27 28class RunKwargs(LineKwargs): 29 """Keyword arguments for the run_plot function.""" 30 31 threshold: NotRequired[float] 32 highlight: NotRequired[str | Sequence[str]] 33 direction: NotRequired[str] 34 35 36# --- functions 37 38 39def _identify_runs( 40 series: Series, 41 threshold: float, 42 up: bool, # False means down 43) -> tuple[Series, Series]: 44 """Identify monotonic increasing/decreasing runs.""" 45 46 diffed = series.diff() 47 change_points = concat([diffed[diffed.gt(threshold)], diffed[diffed.lt(-threshold)]]).sort_index() 48 if series.index[0] not in change_points.index: 49 starting_point = Series([0], index=[series.index[0]]) 50 change_points = concat([change_points, starting_point]).sort_index() 51 facing = change_points > 0 if up else change_points < 0 52 cycles = (facing & ~facing.shift().astype(bool)).cumsum() 53 return cycles[facing], change_points 54 55 56def _plot_runs( 57 axes: Axes, 58 series: Series, 59 up: bool, 60 **kwargs, 61) -> None: 62 """Highlight the runs of a series.""" 63 64 threshold = kwargs["threshold"] 65 match kwargs.get("highlight"): # make sure highlight is a color string 66 case str(): 67 highlight = kwargs.get("highlight") 68 case Sequence(): 69 highlight = kwargs["highlight"][0] if up else kwargs["highlight"][1] 70 case _: 71 raise ValueError( 72 f"Invalid type for highlight: {type(kwargs.get('highlight'))}. Expected str or Sequence." 73 ) 74 75 # highlight the runs 76 stretches, change_points = _identify_runs(series, threshold, up=up) 77 for k in range(1, stretches.max() + 1): 78 stretch = stretches[stretches == k] 79 axes.axvspan( 80 stretch.index.min(), 81 stretch.index.max(), 82 color=highlight, 83 zorder=-1, 84 ) 85 space_above = series.max() - series[stretch.index].max() 86 space_below = series[stretch.index].min() - series.min() 87 y_pos, vert_align = (series.max(), "top") if space_above > space_below else (series.min(), "bottom") 88 text = axes.text( 89 x=stretch.index.min(), 90 y=y_pos, 91 s=(change_points[stretch.index].sum().round(kwargs["rounding"]).astype(str) + " pp"), 92 va=vert_align, 93 ha="left", 94 fontsize="x-small", 95 rotation=90, 96 ) 97 text.set_path_effects([pe.withStroke(linewidth=5, foreground="w")]) 98 99 100def run_plot(data: DataT, **kwargs: Unpack[RunKwargs]) -> Axes: 101 """Plot a series of percentage rates, highlighting the increasing runs. 102 103 Arguments 104 - data - ordered pandas Series of percentages, with PeriodIndex 105 - **kwargs: RunKwargs 106 107 Return 108 - matplotlib Axes object""" 109 110 # --- check the kwargs 111 report_kwargs(caller="run_plot", **kwargs) 112 validate_kwargs(schema=RunKwargs, caller=ME, **kwargs) 113 114 # --- check the data 115 series = check_clean_timeseries(data, ME) 116 if not isinstance(series, Series): 117 raise TypeError("series must be a pandas Series for run_plot()") 118 series, kwargs_d = constrain_data(series, **kwargs) 119 120 # --- convert PeriodIndex if needed 121 saved_pi = map_periodindex(series) 122 if saved_pi is not None: 123 series = saved_pi[0] 124 125 # --- default arguments - in **kwargs_d 126 kwargs_d["threshold"] = kwargs_d.get("threshold", 0.1) 127 kwargs_d["direction"] = kwargs_d.get("direction", "both") 128 kwargs_d["rounding"] = kwargs_d.get("rounding", 2) 129 kwargs_d["highlight"] = kwargs_d.get( 130 "highlight", 131 ( 132 ("gold", "skyblue") 133 if kwargs_d["direction"] == "both" 134 else "gold" 135 if kwargs_d["direction"] == "up" 136 else "skyblue" 137 ), 138 ) 139 kwargs_d["color"] = kwargs_d.get("color", "darkblue") 140 141 # --- plot the line 142 kwargs_d["drawstyle"] = kwargs_d.get("drawstyle", "steps-post") 143 lp_kwargs = limit_kwargs(LineKwargs, **kwargs_d) 144 axes = line_plot(series, **lp_kwargs) 145 146 # plot the runs 147 match kwargs["direction"]: 148 case "up": 149 _plot_runs(axes, series, up=True, **kwargs_d) 150 case "down": 151 _plot_runs(axes, series, up=False, **kwargs_d) 152 case "both": 153 _plot_runs(axes, series, up=True, **kwargs_d) 154 _plot_runs(axes, series, up=False, **kwargs_d) 155 case _: 156 raise ValueError( 157 f"Invalid value for direction: {kwargs['direction']}. Expected 'up', 'down', or 'both'." 158 ) 159 160 # --- set the labels 161 if saved_pi is not None: 162 set_labels(axes, saved_pi[1], kwargs.get("max_ticks", get_setting("max_ticks"))) 163 164 return axes
ME =
'run_plot'
29class RunKwargs(LineKwargs): 30 """Keyword arguments for the run_plot function.""" 31 32 threshold: NotRequired[float] 33 highlight: NotRequired[str | Sequence[str]] 34 direction: NotRequired[str]
Keyword arguments for the run_plot function.
101def run_plot(data: DataT, **kwargs: Unpack[RunKwargs]) -> Axes: 102 """Plot a series of percentage rates, highlighting the increasing runs. 103 104 Arguments 105 - data - ordered pandas Series of percentages, with PeriodIndex 106 - **kwargs: RunKwargs 107 108 Return 109 - matplotlib Axes object""" 110 111 # --- check the kwargs 112 report_kwargs(caller="run_plot", **kwargs) 113 validate_kwargs(schema=RunKwargs, caller=ME, **kwargs) 114 115 # --- check the data 116 series = check_clean_timeseries(data, ME) 117 if not isinstance(series, Series): 118 raise TypeError("series must be a pandas Series for run_plot()") 119 series, kwargs_d = constrain_data(series, **kwargs) 120 121 # --- convert PeriodIndex if needed 122 saved_pi = map_periodindex(series) 123 if saved_pi is not None: 124 series = saved_pi[0] 125 126 # --- default arguments - in **kwargs_d 127 kwargs_d["threshold"] = kwargs_d.get("threshold", 0.1) 128 kwargs_d["direction"] = kwargs_d.get("direction", "both") 129 kwargs_d["rounding"] = kwargs_d.get("rounding", 2) 130 kwargs_d["highlight"] = kwargs_d.get( 131 "highlight", 132 ( 133 ("gold", "skyblue") 134 if kwargs_d["direction"] == "both" 135 else "gold" 136 if kwargs_d["direction"] == "up" 137 else "skyblue" 138 ), 139 ) 140 kwargs_d["color"] = kwargs_d.get("color", "darkblue") 141 142 # --- plot the line 143 kwargs_d["drawstyle"] = kwargs_d.get("drawstyle", "steps-post") 144 lp_kwargs = limit_kwargs(LineKwargs, **kwargs_d) 145 axes = line_plot(series, **lp_kwargs) 146 147 # plot the runs 148 match kwargs["direction"]: 149 case "up": 150 _plot_runs(axes, series, up=True, **kwargs_d) 151 case "down": 152 _plot_runs(axes, series, up=False, **kwargs_d) 153 case "both": 154 _plot_runs(axes, series, up=True, **kwargs_d) 155 _plot_runs(axes, series, up=False, **kwargs_d) 156 case _: 157 raise ValueError( 158 f"Invalid value for direction: {kwargs['direction']}. Expected 'up', 'down', or 'both'." 159 ) 160 161 # --- set the labels 162 if saved_pi is not None: 163 set_labels(axes, saved_pi[1], kwargs.get("max_ticks", get_setting("max_ticks"))) 164 165 return axes
Plot a series of percentage rates, highlighting the increasing runs.
Arguments
- data - ordered pandas Series of percentages, with PeriodIndex
- **kwargs: RunKwargs
Return
- matplotlib Axes object