mgplot.growth_plot
growth_plot.py: plot period and annual/through-the-year growth rates on the same axes.
- calc_growth()
- growth_plot()
- series_growth_plot()
1""" 2growth_plot.py: 3plot period and annual/through-the-year growth rates on the same axes. 4- calc_growth() 5- growth_plot() 6- series_growth_plot() 7""" 8 9# --- imports 10from typing import Unpack, NotRequired, cast 11from pandas import Series, DataFrame, Period, PeriodIndex, period_range 12from numpy import nan 13from matplotlib.pyplot import Axes 14 15from mgplot.bar_plot import bar_plot 16from mgplot.line_plot import line_plot 17from mgplot.axis_utils import map_periodindex 18from mgplot.settings import DataT 19from mgplot.axis_utils import set_labels 20from mgplot.utilities import check_clean_timeseries, constrain_data 21from mgplot.keyword_checking import ( 22 validate_kwargs, 23 report_kwargs, 24 BaseKwargs, 25 TransitionKwargs, 26 package_kwargs, 27) 28 29# === constants 30 31 32# - overarching constants 33class GrowthKwargs(BaseKwargs): 34 """Keyword arguments for the growth_plot function.""" 35 36 # --- common options 37 ax: NotRequired[Axes | None] 38 plot_from: NotRequired[int | Period] 39 label_series: NotRequired[bool] 40 max_ticks: NotRequired[int] 41 # --- options passed to the line plot 42 line_width: NotRequired[float | int] 43 line_color: NotRequired[str] 44 line_style: NotRequired[str] 45 annotate_line: NotRequired[bool] 46 line_rounding: NotRequired[bool | int] 47 line_fontsize: NotRequired[str | int | float] 48 line_fontname: NotRequired[str] 49 line_anno_color: NotRequired[str] 50 # --- options passed to the bar plot 51 annotate_bars: NotRequired[bool] 52 bar_fontsize: NotRequired[str | int | float] 53 bar_fontname: NotRequired[str] 54 bar_rounding: NotRequired[int] 55 bar_width: NotRequired[float] 56 bar_color: NotRequired[str] 57 bar_anno_color: NotRequired[str] 58 bar_rotation: NotRequired[int | float] 59 60 61class SeriesGrowthKwargs(GrowthKwargs): 62 """Keyword arguments for the series_growth_plot function.""" 63 64 ylabel: NotRequired[str | None] 65 66 67# - transition of kwargs from growth_plot to line_plot 68common_transitions: TransitionKwargs = { 69 # arg-to-growth_plot : (arg-to-line_plot, default_value) 70 "label_series": ("label_series", True), 71 "ax": ("ax", None), 72 "max_ticks": ("max_ticks", None), 73 "plot_from": ("plot_from", None), 74 "report_kwargs": ("report_kwargs", None), 75} 76 77to_line_plot: TransitionKwargs = common_transitions | { 78 # arg-to-growth_plot : (arg-to-line_plot, default_value) 79 "line_width": ("width", None), 80 "line_color": ("color", "darkblue"), 81 "line_style": ("style", None), 82 "annotate_line": ("annotate", True), 83 "line_rounding": ("rounding", None), 84 "line_fontsize": ("fontsize", None), 85 "line_fontname": ("fontname", None), 86 "line_anno_color": ("annotate_color", None), 87} 88 89# - constants for the bar plot 90to_bar_plot: TransitionKwargs = common_transitions | { 91 # arg-to-growth_plot : (arg-to-bar_plot, default_value) 92 "bar_width": ("width", 0.8), 93 "bar_color": ("color", "#dd0000"), 94 "annotate_bars": ("annotate", True), 95 "bar_rounding": ("rounding", None), 96 "above": ("above", False), 97 "bar_rotation": ("rotation", None), 98 "bar_fontsize": ("fontsize", None), 99 "bar_fontname": ("fontname", None), 100 "bar_anno_color": ("annotate_color", None), 101} 102 103 104# === functions 105# --- public functions 106def calc_growth(series: Series) -> DataFrame: 107 """ 108 Calculate annual and periodic growth for a pandas Series, 109 where the index is a PeriodIndex. 110 111 Args: 112 - series: A pandas Series with an appropriate PeriodIndex. 113 114 Returns a two column DataFrame: 115 116 Raises 117 - TypeError if the series is not a pandas Series. 118 - TypeError if the series index is not a PeriodIndex. 119 - ValueError if the series is empty. 120 - ValueError if the series index does not have a frequency of Q, M, or D. 121 - ValueError if the series index has duplicates. 122 """ 123 124 # --- sanity checks 125 if not isinstance(series, Series): 126 raise TypeError("The series argument must be a pandas Series") 127 if not isinstance(series.index, PeriodIndex): 128 raise TypeError("The series index must be a pandas PeriodIndex") 129 if series.empty: 130 raise ValueError("The series argument must not be empty") 131 if series.index.freqstr[0] not in ("Q", "M", "D"): 132 raise ValueError("The series index must have a frequency of Q, M, or D") 133 if series.index.has_duplicates: 134 raise ValueError("The series index must not have duplicate values") 135 136 # --- ensure the index is complete and the date is sorted 137 complete = period_range(start=series.index.min(), end=series.index.max()) 138 series = series.reindex(complete, fill_value=nan) 139 series = series.sort_index(ascending=True) 140 141 # --- calculate annual and periodic growth 142 ppy = {"Q": 4, "M": 12, "D": 365}[PeriodIndex(series.index).freqstr[:1]] 143 annual = series.pct_change(periods=ppy) * 100 144 periodic = series.pct_change(periods=1) * 100 145 periodic_name = {4: "Quarterly", 12: "Monthly", 365: "Daily"}[ppy] + " Growth" 146 return DataFrame( 147 { 148 "Annual Growth": annual, 149 periodic_name: periodic, 150 } 151 ) 152 153 154def growth_plot( 155 data: DataT, 156 **kwargs: Unpack[GrowthKwargs], 157) -> Axes: 158 """ 159 Plot annual growth (as a line) and periodic growth (as bars) 160 on the same axes. 161 162 Args: 163 - data: A pandas DataFrame with two columns: 164 - kwargs: GrowthKwargs 165 166 Returns: 167 - axes: The matplotlib Axes object. 168 169 Raises: 170 - TypeError if the annual and periodic arguments are not pandas Series. 171 - TypeError if the annual index is not a PeriodIndex. 172 - ValueError if the annual and periodic series do not have the same index. 173 """ 174 175 # --- check the kwargs 176 me = "growth_plot" 177 report_kwargs(caller=me, **kwargs) 178 validate_kwargs(GrowthKwargs, caller=me, **kwargs) 179 180 # --- data checks 181 data = check_clean_timeseries(data, me) 182 if len(data.columns) != 2: 183 raise TypeError("The data argument must be a pandas DataFrame with two columns") 184 data, kwargsd = constrain_data(data, **kwargs) 185 186 # --- get the series of interest ... 187 annual = data[data.columns[0]] 188 periodic = data[data.columns[1]] 189 190 # --- series names 191 annual.name = "Annual Growth" 192 periodic.name = {"M": "Monthly", "Q": "Quarterly", "D": "Daily"}[ 193 PeriodIndex(periodic.index).freqstr[:1] 194 ] + " Growth" 195 196 # --- convert PeriodIndex periodic growth data to integer indexed data. 197 saved_pi = map_periodindex(periodic) 198 if saved_pi is not None: 199 periodic = saved_pi[0] # extract the reindexed DataFrame 200 201 # --- simple bar chart for the periodic growth 202 if "bar_anno_color" not in kwargsd or kwargsd["bar_anno_color"] is None: 203 kwargsd["bar_anno_color"] = "black" if kwargsd.get("above", False) else "white" 204 selected = package_kwargs(to_bar_plot, **kwargsd) 205 axes = bar_plot(periodic, **selected) 206 207 # --- and now the annual growth as a line 208 selected = package_kwargs(to_line_plot, **kwargsd) 209 line_plot(annual, ax=axes, **selected) 210 211 # --- fix the x-axis labels 212 if saved_pi is not None: 213 set_labels(axes, saved_pi[1], kwargsd.get("max_ticks", 10)) 214 215 # --- and done ... 216 return axes 217 218 219def series_growth_plot( 220 data: DataT, 221 **kwargs: Unpack[SeriesGrowthKwargs], 222) -> Axes: 223 """ 224 Plot annual and periodic growth in percentage terms from 225 a pandas Series, and finalise the plot. 226 227 Args: 228 - data: A pandas Series with an appropriate PeriodIndex. 229 - kwargs: SeriesGrowthKwargs 230 - takes much the same kwargs as for growth_plot() 231 """ 232 233 # --- check the kwargs 234 me = "series_growth_plot" 235 report_kwargs(caller=me, **kwargs) 236 validate_kwargs(SeriesGrowthKwargs, caller=me, **kwargs) 237 238 # --- sanity checks 239 if not isinstance(data, Series): 240 raise TypeError("The data argument to series_growth_plot() must be a pandas Series") 241 242 # --- calculate growth and plot - add ylabel 243 ylabel: str | None = kwargs.pop("ylabel", None) 244 if ylabel is not None: 245 print(f"Did you intend to specify a value for the 'ylabel' in {me}()?") 246 ylabel = "Growth (%)" if ylabel is None else ylabel 247 growth = calc_growth(data) 248 ax = growth_plot(growth, **cast(GrowthKwargs, kwargs)) 249 ax.set_ylabel(ylabel) 250 return ax
class
GrowthKwargs(mgplot.keyword_checking.BaseKwargs):
34class GrowthKwargs(BaseKwargs): 35 """Keyword arguments for the growth_plot function.""" 36 37 # --- common options 38 ax: NotRequired[Axes | None] 39 plot_from: NotRequired[int | Period] 40 label_series: NotRequired[bool] 41 max_ticks: NotRequired[int] 42 # --- options passed to the line plot 43 line_width: NotRequired[float | int] 44 line_color: NotRequired[str] 45 line_style: NotRequired[str] 46 annotate_line: NotRequired[bool] 47 line_rounding: NotRequired[bool | int] 48 line_fontsize: NotRequired[str | int | float] 49 line_fontname: NotRequired[str] 50 line_anno_color: NotRequired[str] 51 # --- options passed to the bar plot 52 annotate_bars: NotRequired[bool] 53 bar_fontsize: NotRequired[str | int | float] 54 bar_fontname: NotRequired[str] 55 bar_rounding: NotRequired[int] 56 bar_width: NotRequired[float] 57 bar_color: NotRequired[str] 58 bar_anno_color: NotRequired[str] 59 bar_rotation: NotRequired[int | float]
Keyword arguments for the growth_plot function.
62class SeriesGrowthKwargs(GrowthKwargs): 63 """Keyword arguments for the series_growth_plot function.""" 64 65 ylabel: NotRequired[str | None]
Keyword arguments for the series_growth_plot function.
common_transitions: mgplot.keyword_checking.TransitionKwargs =
{'label_series': ('label_series', True), 'ax': ('ax', None), 'max_ticks': ('max_ticks', None), 'plot_from': ('plot_from', None), 'report_kwargs': ('report_kwargs', None)}
to_line_plot: mgplot.keyword_checking.TransitionKwargs =
{'label_series': ('label_series', True), 'ax': ('ax', None), 'max_ticks': ('max_ticks', None), 'plot_from': ('plot_from', None), 'report_kwargs': ('report_kwargs', None), 'line_width': ('width', None), 'line_color': ('color', 'darkblue'), 'line_style': ('style', None), 'annotate_line': ('annotate', True), 'line_rounding': ('rounding', None), 'line_fontsize': ('fontsize', None), 'line_fontname': ('fontname', None), 'line_anno_color': ('annotate_color', None)}
to_bar_plot: mgplot.keyword_checking.TransitionKwargs =
{'label_series': ('label_series', True), 'ax': ('ax', None), 'max_ticks': ('max_ticks', None), 'plot_from': ('plot_from', None), 'report_kwargs': ('report_kwargs', None), 'bar_width': ('width', 0.8), 'bar_color': ('color', '#dd0000'), 'annotate_bars': ('annotate', True), 'bar_rounding': ('rounding', None), 'above': ('above', False), 'bar_rotation': ('rotation', None), 'bar_fontsize': ('fontsize', None), 'bar_fontname': ('fontname', None), 'bar_anno_color': ('annotate_color', None)}
def
calc_growth(series: pandas.core.series.Series) -> pandas.core.frame.DataFrame:
107def calc_growth(series: Series) -> DataFrame: 108 """ 109 Calculate annual and periodic growth for a pandas Series, 110 where the index is a PeriodIndex. 111 112 Args: 113 - series: A pandas Series with an appropriate PeriodIndex. 114 115 Returns a two column DataFrame: 116 117 Raises 118 - TypeError if the series is not a pandas Series. 119 - TypeError if the series index is not a PeriodIndex. 120 - ValueError if the series is empty. 121 - ValueError if the series index does not have a frequency of Q, M, or D. 122 - ValueError if the series index has duplicates. 123 """ 124 125 # --- sanity checks 126 if not isinstance(series, Series): 127 raise TypeError("The series argument must be a pandas Series") 128 if not isinstance(series.index, PeriodIndex): 129 raise TypeError("The series index must be a pandas PeriodIndex") 130 if series.empty: 131 raise ValueError("The series argument must not be empty") 132 if series.index.freqstr[0] not in ("Q", "M", "D"): 133 raise ValueError("The series index must have a frequency of Q, M, or D") 134 if series.index.has_duplicates: 135 raise ValueError("The series index must not have duplicate values") 136 137 # --- ensure the index is complete and the date is sorted 138 complete = period_range(start=series.index.min(), end=series.index.max()) 139 series = series.reindex(complete, fill_value=nan) 140 series = series.sort_index(ascending=True) 141 142 # --- calculate annual and periodic growth 143 ppy = {"Q": 4, "M": 12, "D": 365}[PeriodIndex(series.index).freqstr[:1]] 144 annual = series.pct_change(periods=ppy) * 100 145 periodic = series.pct_change(periods=1) * 100 146 periodic_name = {4: "Quarterly", 12: "Monthly", 365: "Daily"}[ppy] + " Growth" 147 return DataFrame( 148 { 149 "Annual Growth": annual, 150 periodic_name: periodic, 151 } 152 )
Calculate annual and periodic growth for a pandas Series, where the index is a PeriodIndex.
Args:
- series: A pandas Series with an appropriate PeriodIndex.
Returns a two column DataFrame:
Raises
- TypeError if the series is not a pandas Series.
- TypeError if the series index is not a PeriodIndex.
- ValueError if the series is empty.
- ValueError if the series index does not have a frequency of Q, M, or D.
- ValueError if the series index has duplicates.
155def growth_plot( 156 data: DataT, 157 **kwargs: Unpack[GrowthKwargs], 158) -> Axes: 159 """ 160 Plot annual growth (as a line) and periodic growth (as bars) 161 on the same axes. 162 163 Args: 164 - data: A pandas DataFrame with two columns: 165 - kwargs: GrowthKwargs 166 167 Returns: 168 - axes: The matplotlib Axes object. 169 170 Raises: 171 - TypeError if the annual and periodic arguments are not pandas Series. 172 - TypeError if the annual index is not a PeriodIndex. 173 - ValueError if the annual and periodic series do not have the same index. 174 """ 175 176 # --- check the kwargs 177 me = "growth_plot" 178 report_kwargs(caller=me, **kwargs) 179 validate_kwargs(GrowthKwargs, caller=me, **kwargs) 180 181 # --- data checks 182 data = check_clean_timeseries(data, me) 183 if len(data.columns) != 2: 184 raise TypeError("The data argument must be a pandas DataFrame with two columns") 185 data, kwargsd = constrain_data(data, **kwargs) 186 187 # --- get the series of interest ... 188 annual = data[data.columns[0]] 189 periodic = data[data.columns[1]] 190 191 # --- series names 192 annual.name = "Annual Growth" 193 periodic.name = {"M": "Monthly", "Q": "Quarterly", "D": "Daily"}[ 194 PeriodIndex(periodic.index).freqstr[:1] 195 ] + " Growth" 196 197 # --- convert PeriodIndex periodic growth data to integer indexed data. 198 saved_pi = map_periodindex(periodic) 199 if saved_pi is not None: 200 periodic = saved_pi[0] # extract the reindexed DataFrame 201 202 # --- simple bar chart for the periodic growth 203 if "bar_anno_color" not in kwargsd or kwargsd["bar_anno_color"] is None: 204 kwargsd["bar_anno_color"] = "black" if kwargsd.get("above", False) else "white" 205 selected = package_kwargs(to_bar_plot, **kwargsd) 206 axes = bar_plot(periodic, **selected) 207 208 # --- and now the annual growth as a line 209 selected = package_kwargs(to_line_plot, **kwargsd) 210 line_plot(annual, ax=axes, **selected) 211 212 # --- fix the x-axis labels 213 if saved_pi is not None: 214 set_labels(axes, saved_pi[1], kwargsd.get("max_ticks", 10)) 215 216 # --- and done ... 217 return axes
Plot annual growth (as a line) and periodic growth (as bars) on the same axes.
Args:
- data: A pandas DataFrame with two columns:
- kwargs: GrowthKwargs
Returns:
- axes: The matplotlib Axes object.
Raises:
- TypeError if the annual and periodic arguments are not pandas Series.
- TypeError if the annual index is not a PeriodIndex.
- ValueError if the annual and periodic series do not have the same index.
def
series_growth_plot( data: ~DataT, **kwargs: Unpack[SeriesGrowthKwargs]) -> matplotlib.axes._axes.Axes:
220def series_growth_plot( 221 data: DataT, 222 **kwargs: Unpack[SeriesGrowthKwargs], 223) -> Axes: 224 """ 225 Plot annual and periodic growth in percentage terms from 226 a pandas Series, and finalise the plot. 227 228 Args: 229 - data: A pandas Series with an appropriate PeriodIndex. 230 - kwargs: SeriesGrowthKwargs 231 - takes much the same kwargs as for growth_plot() 232 """ 233 234 # --- check the kwargs 235 me = "series_growth_plot" 236 report_kwargs(caller=me, **kwargs) 237 validate_kwargs(SeriesGrowthKwargs, caller=me, **kwargs) 238 239 # --- sanity checks 240 if not isinstance(data, Series): 241 raise TypeError("The data argument to series_growth_plot() must be a pandas Series") 242 243 # --- calculate growth and plot - add ylabel 244 ylabel: str | None = kwargs.pop("ylabel", None) 245 if ylabel is not None: 246 print(f"Did you intend to specify a value for the 'ylabel' in {me}()?") 247 ylabel = "Growth (%)" if ylabel is None else ylabel 248 growth = calc_growth(data) 249 ax = growth_plot(growth, **cast(GrowthKwargs, kwargs)) 250 ax.set_ylabel(ylabel) 251 return ax
Plot annual and periodic growth in percentage terms from a pandas Series, and finalise the plot.
Args:
- data: A pandas Series with an appropriate PeriodIndex.
- kwargs: SeriesGrowthKwargs
- takes much the same kwargs as for growth_plot()