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