mgplot 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mgplot/__init__.py +121 -0
- mgplot/bar_plot.py +107 -0
- mgplot/colors.py +199 -0
- mgplot/date_utils.py +324 -0
- mgplot/finalise_plot.py +335 -0
- mgplot/finalisers.py +364 -0
- mgplot/growth_plot.py +275 -0
- mgplot/kw_type_checking.py +460 -0
- mgplot/line_plot.py +178 -0
- mgplot/multi_plot.py +339 -0
- mgplot/postcovid_plot.py +106 -0
- mgplot/py.typed +1 -0
- mgplot/revision_plot.py +60 -0
- mgplot/run_plot.py +182 -0
- mgplot/seastrend_plot.py +74 -0
- mgplot/settings.py +164 -0
- mgplot/summary_plot.py +240 -0
- mgplot/test.py +31 -0
- mgplot/utilities.py +254 -0
- mgplot-0.1.0.dist-info/METADATA +53 -0
- mgplot-0.1.0.dist-info/RECORD +24 -0
- mgplot-0.1.0.dist-info/WHEEL +5 -0
- mgplot-0.1.0.dist-info/licenses/LICENSE +8 -0
- mgplot-0.1.0.dist-info/top_level.txt +1 -0
mgplot/multi_plot.py
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
"""
|
|
2
|
+
multi_plot.py
|
|
3
|
+
|
|
4
|
+
This module provides a function to create multiple plots
|
|
5
|
+
from a single dataset
|
|
6
|
+
- multi_start()
|
|
7
|
+
- multi_column()
|
|
8
|
+
And to chain a plotting function with the finalise_plot() function.
|
|
9
|
+
- plot_then_finalise()
|
|
10
|
+
|
|
11
|
+
Underlying assumptions:
|
|
12
|
+
- every plot function:
|
|
13
|
+
- has a mandatory data: DataFrame | Series argument first (noting
|
|
14
|
+
that some plotting functions only work with Series data, and they
|
|
15
|
+
will raise an error if they are passed a DataFrame).
|
|
16
|
+
- accepts an optional plot_from: int | Period keyword argument
|
|
17
|
+
- returns a matplotlib Axes object
|
|
18
|
+
- the multi functions (all in this module)
|
|
19
|
+
- have a mandatory data: DataFrame | Series argument
|
|
20
|
+
- have a mandatory function: Callable | list[Callable] argument
|
|
21
|
+
and otherwise pass their kwargs to the next function
|
|
22
|
+
when execution is transferred to the next function.
|
|
23
|
+
- the multi functions can be chained together.
|
|
24
|
+
- return None.
|
|
25
|
+
|
|
26
|
+
And why are these three public functions all in the same modules?
|
|
27
|
+
- They all work with the same underlying assumptions.
|
|
28
|
+
- They all take a function argument/list to which execution is
|
|
29
|
+
passed.
|
|
30
|
+
- They all use the same underlying logic to extract the first
|
|
31
|
+
function from the function argument, and to store any remaining
|
|
32
|
+
functions in the kwargs['function'] argument.
|
|
33
|
+
|
|
34
|
+
Note: rather than pass the kwargs dict directly, we will re-pack-it
|
|
35
|
+
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
# --- imports
|
|
39
|
+
from typing import Any, Callable, Final
|
|
40
|
+
from collections.abc import Iterable
|
|
41
|
+
from pandas import Period, DataFrame, Series, period_range
|
|
42
|
+
from numpy import random
|
|
43
|
+
|
|
44
|
+
from mgplot.kw_type_checking import (
|
|
45
|
+
limit_kwargs,
|
|
46
|
+
ExpectedTypeDict,
|
|
47
|
+
report_kwargs,
|
|
48
|
+
)
|
|
49
|
+
from mgplot.finalise_plot import finalise_plot, FINALISE_KW_TYPES
|
|
50
|
+
from mgplot.settings import DataT
|
|
51
|
+
from mgplot.test import prepare_for_test
|
|
52
|
+
from mgplot.utilities import check_clean_timeseries
|
|
53
|
+
|
|
54
|
+
from mgplot.line_plot import line_plot, LP_KW_TYPES
|
|
55
|
+
from mgplot.bar_plot import bar_plot, BAR_PLOT_KW_TYPES
|
|
56
|
+
from mgplot.seastrend_plot import seastrend_plot
|
|
57
|
+
from mgplot.postcovid_plot import postcovid_plot
|
|
58
|
+
from mgplot.revision_plot import revision_plot, REVISION_KW_TYPES
|
|
59
|
+
from mgplot.run_plot import run_plot, RUN_KW_TYPES
|
|
60
|
+
from mgplot.summary_plot import summary_plot, SUMMARY_KW_TYPES
|
|
61
|
+
from mgplot.growth_plot import series_growth_plot, raw_growth_plot, GROWTH_KW_TYPES
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# --- constants
|
|
65
|
+
EXPECTED_CALLABLES: Final[dict[Callable, ExpectedTypeDict]] = {
|
|
66
|
+
# used by plot_then_finalise() to (1) check the target function
|
|
67
|
+
# is one of the expected functions, and (2) to limit the kwargs
|
|
68
|
+
# passed on, to the expected keyword arguments for that function.
|
|
69
|
+
line_plot: LP_KW_TYPES,
|
|
70
|
+
bar_plot: BAR_PLOT_KW_TYPES,
|
|
71
|
+
seastrend_plot: LP_KW_TYPES, # just calls line_plot under the hood
|
|
72
|
+
postcovid_plot: LP_KW_TYPES, # just calls line_plot under the hood
|
|
73
|
+
revision_plot: REVISION_KW_TYPES,
|
|
74
|
+
run_plot: LP_KW_TYPES | RUN_KW_TYPES,
|
|
75
|
+
summary_plot: SUMMARY_KW_TYPES,
|
|
76
|
+
series_growth_plot: GROWTH_KW_TYPES,
|
|
77
|
+
raw_growth_plot: GROWTH_KW_TYPES,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# --- private functions
|
|
82
|
+
def first_unchain(
|
|
83
|
+
function: Callable | list[Callable],
|
|
84
|
+
**kwargs,
|
|
85
|
+
) -> tuple[Callable, dict[str, Any]]:
|
|
86
|
+
"""
|
|
87
|
+
Extract the first Callable from function (which may be
|
|
88
|
+
a stand alone Callable or a nonr-empty list of Callables).
|
|
89
|
+
Store the remaining Callables in kwargs['function'].
|
|
90
|
+
This allows for chaining multiple functions together.
|
|
91
|
+
|
|
92
|
+
Parameters
|
|
93
|
+
- kwargs - keyword arguments
|
|
94
|
+
|
|
95
|
+
Returns a tuple containing the first function and the updated kwargs.
|
|
96
|
+
if function is a list of Callables, the first function will be removed
|
|
97
|
+
from the the list, and the remaining functions will be stored in a
|
|
98
|
+
list under the key "function" in kwargs.
|
|
99
|
+
|
|
100
|
+
Raises ValueError if function is an empty list.
|
|
101
|
+
|
|
102
|
+
Not intended for direct use by the user.
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
error_msg = "function must be a Callable or a non-empty list of Callables"
|
|
106
|
+
|
|
107
|
+
if isinstance(function, list):
|
|
108
|
+
if len(function) == 0:
|
|
109
|
+
raise ValueError(error_msg)
|
|
110
|
+
first, *rest = function
|
|
111
|
+
elif callable(function):
|
|
112
|
+
first, rest = function, []
|
|
113
|
+
else:
|
|
114
|
+
raise ValueError(error_msg)
|
|
115
|
+
|
|
116
|
+
if rest:
|
|
117
|
+
kwargs["function"] = rest
|
|
118
|
+
|
|
119
|
+
return first, kwargs
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# --- public functions
|
|
123
|
+
def plot_then_finalise(
|
|
124
|
+
data: DataT,
|
|
125
|
+
function: Callable | list[Callable],
|
|
126
|
+
**kwargs,
|
|
127
|
+
) -> None:
|
|
128
|
+
"""
|
|
129
|
+
Chain a plotting function with the finalise_plot() function.
|
|
130
|
+
This is designed to be the last function in a chain.
|
|
131
|
+
|
|
132
|
+
Parameters
|
|
133
|
+
- data: Series | DataFrame - The data to be plotted.
|
|
134
|
+
- function: Callable | list[Callable] - The plotting function
|
|
135
|
+
to be used.
|
|
136
|
+
- **kwargs: Additional keyword arguments to be passed to
|
|
137
|
+
the plotting function, and then the finalise_plot() function.
|
|
138
|
+
|
|
139
|
+
Returns None.
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
# --- sanity checks
|
|
143
|
+
report_kwargs(called_from="plot_then_finalise", **kwargs)
|
|
144
|
+
data = check_clean_timeseries(data)
|
|
145
|
+
|
|
146
|
+
# --- check the function argument
|
|
147
|
+
first, kwargs_ = first_unchain(function, **kwargs)
|
|
148
|
+
|
|
149
|
+
if first in EXPECTED_CALLABLES:
|
|
150
|
+
expected = EXPECTED_CALLABLES[first]
|
|
151
|
+
plot_kwargs = limit_kwargs(expected, **kwargs)
|
|
152
|
+
else:
|
|
153
|
+
# this is an unexpected Callable, so we will give it a try
|
|
154
|
+
print(f"Unknown proposed function: {first}; nonetheless, will give it a try.")
|
|
155
|
+
plot_kwargs = kwargs_
|
|
156
|
+
|
|
157
|
+
# --- call the first function with the data and kwargs
|
|
158
|
+
|
|
159
|
+
axes = first(data, **plot_kwargs)
|
|
160
|
+
|
|
161
|
+
# --- finalise the plot
|
|
162
|
+
fp_kwargs = limit_kwargs(FINALISE_KW_TYPES, **kwargs)
|
|
163
|
+
finalise_plot(axes, **fp_kwargs)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def multi_start(
|
|
167
|
+
data: DataT,
|
|
168
|
+
function: Callable | list[Callable],
|
|
169
|
+
starts: Iterable[None | Period | int],
|
|
170
|
+
**kwargs,
|
|
171
|
+
) -> None:
|
|
172
|
+
"""
|
|
173
|
+
Create multiple plots with different starting points.
|
|
174
|
+
Each plot will start from the specified starting point.
|
|
175
|
+
|
|
176
|
+
Parameters
|
|
177
|
+
- data: Series | DataFrame - The data to be plotted.
|
|
178
|
+
- function: Callable | list[Callable] - The plotting function
|
|
179
|
+
to be used.
|
|
180
|
+
- starts: Iterable[Period | int | None] - The starting points
|
|
181
|
+
for each plot (None means use the entire data).
|
|
182
|
+
- **kwargs: Additional keyword arguments to be passed to
|
|
183
|
+
the plotting function.
|
|
184
|
+
|
|
185
|
+
Returns None.
|
|
186
|
+
|
|
187
|
+
Raises
|
|
188
|
+
- ValueError if the starts is not an iterable of None, Period or int.
|
|
189
|
+
|
|
190
|
+
Note: kwargs['tag'] is used to create a unique tag for each plot.
|
|
191
|
+
"""
|
|
192
|
+
|
|
193
|
+
# --- sanity checks
|
|
194
|
+
report_kwargs(called_from="multi_start", **kwargs)
|
|
195
|
+
data = check_clean_timeseries(data)
|
|
196
|
+
if not isinstance(starts, Iterable):
|
|
197
|
+
raise ValueError("starts must be an iterable of None, Period or int")
|
|
198
|
+
|
|
199
|
+
# --- check the function argument
|
|
200
|
+
original_tag: Final[str] = kwargs.get("tag", "")
|
|
201
|
+
first, kwargs = first_unchain(function, **kwargs)
|
|
202
|
+
|
|
203
|
+
# --- iterate over the starts
|
|
204
|
+
for i, start in enumerate(starts):
|
|
205
|
+
kw = kwargs.copy() # copy to avoid modifying the original kwargs
|
|
206
|
+
this_tag = f"{original_tag}_{i}"
|
|
207
|
+
kw["tag"] = this_tag
|
|
208
|
+
kw["plot_from"] = start # rely on plotting function to constrain the data
|
|
209
|
+
first(data, **kw)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def multi_column(
|
|
213
|
+
data: DataFrame,
|
|
214
|
+
function: Callable | list[Callable],
|
|
215
|
+
**kwargs,
|
|
216
|
+
) -> None:
|
|
217
|
+
"""
|
|
218
|
+
Create multiple plots, one for each column in a DataFrame.
|
|
219
|
+
The plot title will be the column name.
|
|
220
|
+
|
|
221
|
+
Parameters
|
|
222
|
+
- data: DataFrame - The data to be plotted.
|
|
223
|
+
- function: Callable - The plotting function to be used.
|
|
224
|
+
- **kwargs: Additional keyword arguments to be passed to
|
|
225
|
+
the plotting function.
|
|
226
|
+
|
|
227
|
+
Returns None.
|
|
228
|
+
"""
|
|
229
|
+
|
|
230
|
+
# --- sanity checks
|
|
231
|
+
report_kwargs(called_from="multi_column", **kwargs)
|
|
232
|
+
data = check_clean_timeseries(data)
|
|
233
|
+
|
|
234
|
+
# --- check the function argument
|
|
235
|
+
title_stem = kwargs.get("title", "")
|
|
236
|
+
tag: Final[str] = kwargs.get("tag", "")
|
|
237
|
+
first, kwargs = first_unchain(function, **kwargs)
|
|
238
|
+
|
|
239
|
+
# --- iterate over the columns
|
|
240
|
+
for i, col in enumerate(data.columns):
|
|
241
|
+
|
|
242
|
+
series = data[[col]]
|
|
243
|
+
kwargs["title"] = f"{title_stem}{col}" if title_stem else col
|
|
244
|
+
|
|
245
|
+
this_tag = f"_{tag}_{i}".replace("__", "_")
|
|
246
|
+
kwargs["tag"] = this_tag
|
|
247
|
+
|
|
248
|
+
first(series, **kwargs)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
# --- test
|
|
252
|
+
if __name__ == "__main__":
|
|
253
|
+
|
|
254
|
+
prepare_for_test("multi_plot")
|
|
255
|
+
|
|
256
|
+
dates = period_range("2020-01-01", "2020-12-31", freq="D")
|
|
257
|
+
df = DataFrame(
|
|
258
|
+
{
|
|
259
|
+
"Series 1": random.rand(len(dates)),
|
|
260
|
+
"Series 2": random.rand(len(dates)),
|
|
261
|
+
"Series 3": random.rand(len(dates)),
|
|
262
|
+
},
|
|
263
|
+
index=dates,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
# Test multi_start
|
|
267
|
+
multi_start(
|
|
268
|
+
df,
|
|
269
|
+
function=[plot_then_finalise, line_plot],
|
|
270
|
+
starts=[None, 50, 100, Period("2020-06-01", freq="D")],
|
|
271
|
+
title="Test Multi Start: ",
|
|
272
|
+
tag="tag_test",
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
# Test multi_column
|
|
276
|
+
multi_column(
|
|
277
|
+
df, function=[plot_then_finalise, line_plot], title="Test Multi Column: "
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
# Test Test Multi Column / Multi start
|
|
281
|
+
multi_column(
|
|
282
|
+
df,
|
|
283
|
+
[multi_start, plot_then_finalise, line_plot],
|
|
284
|
+
title="Test Multi Column / Multi start: ",
|
|
285
|
+
starts=[None, 180],
|
|
286
|
+
verbose=False,
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
# bar plot
|
|
290
|
+
# Test 1
|
|
291
|
+
series_ = Series([1, 2, 3, 4, 5], index=list("ABCDE"))
|
|
292
|
+
plot_then_finalise(
|
|
293
|
+
series_,
|
|
294
|
+
function=bar_plot,
|
|
295
|
+
title="Bar Plot Example 1a",
|
|
296
|
+
xlabel="X-axis Label",
|
|
297
|
+
ylabel="Y-axis Label",
|
|
298
|
+
rotation=45,
|
|
299
|
+
)
|
|
300
|
+
multi_start(
|
|
301
|
+
series_,
|
|
302
|
+
function=[plot_then_finalise, bar_plot],
|
|
303
|
+
starts=[0, -2],
|
|
304
|
+
title="Multi-start Bar Plot Example 1b",
|
|
305
|
+
xlabel="X-axis Label",
|
|
306
|
+
ylabel="Y-axis Label",
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
# --- test
|
|
311
|
+
if __name__ == "__main__":
|
|
312
|
+
|
|
313
|
+
# --- check that this fails
|
|
314
|
+
try:
|
|
315
|
+
multi_start(
|
|
316
|
+
data=DataFrame(),
|
|
317
|
+
function=[plot_then_finalise],
|
|
318
|
+
starts=[0, 1],
|
|
319
|
+
)
|
|
320
|
+
except (ValueError, TypeError) as e:
|
|
321
|
+
print(f"Expected error: {e}")
|
|
322
|
+
|
|
323
|
+
# --- check that tjis fails
|
|
324
|
+
try:
|
|
325
|
+
multi_column(
|
|
326
|
+
data=Series([1, 2, 3]), # type: ignore # Series is not a DataFrame
|
|
327
|
+
function=[plot_then_finalise],
|
|
328
|
+
)
|
|
329
|
+
except (ValueError, TypeError) as e:
|
|
330
|
+
print(f"Expected error: {e}")
|
|
331
|
+
|
|
332
|
+
# --- check that this fails
|
|
333
|
+
try:
|
|
334
|
+
plot_then_finalise(
|
|
335
|
+
data=Series([1, 2, 3]),
|
|
336
|
+
function=[],
|
|
337
|
+
)
|
|
338
|
+
except (ValueError, TypeError) as e:
|
|
339
|
+
print(f"Expected error: {e}")
|
mgplot/postcovid_plot.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""
|
|
2
|
+
covid_recovery_plot.py
|
|
3
|
+
Plot the pre-COVID trajectory against the current trend.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
# --- imports
|
|
7
|
+
from pandas import DataFrame, Series, Period, PeriodIndex
|
|
8
|
+
from matplotlib.pyplot import Axes
|
|
9
|
+
from numpy import arange, polyfit
|
|
10
|
+
|
|
11
|
+
from mgplot.settings import DataT, get_setting
|
|
12
|
+
from mgplot.line_plot import line_plot
|
|
13
|
+
from mgplot.utilities import check_clean_timeseries
|
|
14
|
+
from mgplot.kw_type_checking import report_kwargs
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# --- constants
|
|
18
|
+
WIDTH = "width"
|
|
19
|
+
STYLE = "style"
|
|
20
|
+
START_R = "start_r"
|
|
21
|
+
END_R = "end_r"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# --- functions
|
|
25
|
+
def get_projection(original: Series, to_period: Period) -> Series:
|
|
26
|
+
"""
|
|
27
|
+
Projection based on data from the start of a series
|
|
28
|
+
to the to_period (inclusive). Returns projection over the whole
|
|
29
|
+
period of the original series.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
y_regress = original[original.index <= to_period].copy()
|
|
33
|
+
x_regress = arange(len(y_regress))
|
|
34
|
+
m, b = polyfit(x_regress, y_regress, 1)
|
|
35
|
+
|
|
36
|
+
x_complete = arange(len(original))
|
|
37
|
+
projection = Series((x_complete * m) + b, index=original.index)
|
|
38
|
+
|
|
39
|
+
return projection
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def postcovid_plot(data: DataT, **kwargs) -> Axes:
|
|
43
|
+
"""
|
|
44
|
+
Plots a series with a PeriodIndex.
|
|
45
|
+
|
|
46
|
+
Arguments
|
|
47
|
+
- data - the series to be plotted (note that this function
|
|
48
|
+
is designed to work with a single series, not a DataFrame).
|
|
49
|
+
- **kwargs - same as for line_plot() and finalise_plot().
|
|
50
|
+
|
|
51
|
+
Raises:
|
|
52
|
+
- TypeError if series is not a pandas Series
|
|
53
|
+
- TypeError if series does not have a PeriodIndex
|
|
54
|
+
- ValueError if series does not have a D, M or Q frequency
|
|
55
|
+
- ValueError if regression start is after regression end
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
# --- sanity checks
|
|
59
|
+
report_kwargs(called_from="postcovid_plot", **kwargs)
|
|
60
|
+
data = check_clean_timeseries(data)
|
|
61
|
+
if not isinstance(data, Series):
|
|
62
|
+
raise TypeError("The series argument must be a pandas Series")
|
|
63
|
+
series: Series = data
|
|
64
|
+
series_index = PeriodIndex(series.index) # syntactic sugar for type hinting
|
|
65
|
+
if series_index.freqstr[:1] not in ("Q", "M", "D"):
|
|
66
|
+
raise ValueError("The series index must have a D, M or Q freq")
|
|
67
|
+
# rely on line_plot() to validate kwargs
|
|
68
|
+
if "plot_from" in kwargs:
|
|
69
|
+
print("Warning: the 'plot_from' argument is ignored in postcovid_plot().")
|
|
70
|
+
del kwargs["plot_from"]
|
|
71
|
+
|
|
72
|
+
# --- plot COVID counterfactural
|
|
73
|
+
freq = PeriodIndex(series.index).freqstr # syntactic sugar for type hinting
|
|
74
|
+
match freq[0]:
|
|
75
|
+
case "Q":
|
|
76
|
+
start_regression = Period("2014Q4", freq=freq)
|
|
77
|
+
end_regression = Period("2019Q4", freq=freq)
|
|
78
|
+
case "M":
|
|
79
|
+
start_regression = Period("2015-01", freq=freq)
|
|
80
|
+
end_regression = Period("2020-01", freq=freq)
|
|
81
|
+
case "D":
|
|
82
|
+
start_regression = Period("2015-01-01", freq=freq)
|
|
83
|
+
end_regression = Period("2020-01-01", freq=freq)
|
|
84
|
+
|
|
85
|
+
start_regression = Period(kwargs.pop("start_r", start_regression), freq=freq)
|
|
86
|
+
end_regression = Period(kwargs.pop("end_r", end_regression), freq=freq)
|
|
87
|
+
if start_regression >= end_regression:
|
|
88
|
+
raise ValueError("Start period must be before end period")
|
|
89
|
+
|
|
90
|
+
# --- combine data and projection
|
|
91
|
+
recent = series[series.index >= start_regression].copy()
|
|
92
|
+
recent.name = "Series"
|
|
93
|
+
projection = get_projection(recent, end_regression)
|
|
94
|
+
projection.name = "Pre-COVID projection"
|
|
95
|
+
data_set = DataFrame([projection, recent]).T
|
|
96
|
+
|
|
97
|
+
kwargs[WIDTH] = kwargs.pop(
|
|
98
|
+
WIDTH, [get_setting("line_normal"), get_setting("line_wide")]
|
|
99
|
+
)
|
|
100
|
+
kwargs[STYLE] = kwargs.pop(STYLE, ["--", "-"])
|
|
101
|
+
kwargs["legend"] = kwargs.pop("legend", True)
|
|
102
|
+
|
|
103
|
+
return line_plot(
|
|
104
|
+
data_set,
|
|
105
|
+
**kwargs,
|
|
106
|
+
)
|
mgplot/py.typed
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
mgplot/revision_plot.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""
|
|
2
|
+
revision_plot.py
|
|
3
|
+
Plot ABS revisions to estimates over time.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
# --- imports
|
|
7
|
+
from pandas import Series
|
|
8
|
+
from matplotlib.pyplot import Axes
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
from mgplot.utilities import annotate_series, check_clean_timeseries
|
|
12
|
+
from mgplot.line_plot import LP_KW_TYPES, line_plot
|
|
13
|
+
from mgplot.kw_type_checking import validate_kwargs, validate_expected
|
|
14
|
+
from mgplot.kw_type_checking import report_kwargs
|
|
15
|
+
from mgplot.settings import DataT
|
|
16
|
+
from mgplot.kw_type_checking import ExpectedTypeDict
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# --- constants
|
|
20
|
+
ROUNDING = "rounding"
|
|
21
|
+
REVISION_KW_TYPES: ExpectedTypeDict = {
|
|
22
|
+
ROUNDING: (int, bool),
|
|
23
|
+
} | LP_KW_TYPES
|
|
24
|
+
validate_expected(REVISION_KW_TYPES, "revision_plot")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# --- functions
|
|
28
|
+
def revision_plot(data: DataT, **kwargs) -> Axes:
|
|
29
|
+
"""
|
|
30
|
+
Plot the revisions to ABS data.
|
|
31
|
+
|
|
32
|
+
Arguments
|
|
33
|
+
data: pd.DataFrame - the data to plot, the DataFrame has a
|
|
34
|
+
column for each data revision
|
|
35
|
+
recent: int - the number of recent data points to plot
|
|
36
|
+
kwargs : dict :
|
|
37
|
+
- units: str - the units for the data (Note: you may need to
|
|
38
|
+
recalibrate the units for the y-axis)
|
|
39
|
+
- rounding: int | bool - if True apply default rounding, otherwise
|
|
40
|
+
apply int rounding.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
# --- sanity checks
|
|
44
|
+
data = check_clean_timeseries(data)
|
|
45
|
+
report_kwargs(called_from="revision_plot", **kwargs)
|
|
46
|
+
validate_kwargs(REVISION_KW_TYPES, "revision_plot", **kwargs)
|
|
47
|
+
|
|
48
|
+
# --- critical defaults
|
|
49
|
+
kwargs["plot_from"] = kwargs.get("plot_from", -18)
|
|
50
|
+
|
|
51
|
+
# --- plot
|
|
52
|
+
axes = line_plot(data, **kwargs)
|
|
53
|
+
|
|
54
|
+
# --- Annotate the last value in each series ...
|
|
55
|
+
rounding: int | bool = kwargs.pop(ROUNDING, True)
|
|
56
|
+
for c in data.columns:
|
|
57
|
+
col: Series = data.loc[:, c].dropna()
|
|
58
|
+
annotate_series(col, axes, color="#222222", rounding=rounding, fontsize="small")
|
|
59
|
+
|
|
60
|
+
return axes
|
mgplot/run_plot.py
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""
|
|
2
|
+
run_plot.py
|
|
3
|
+
This code contains a function to plot and highlighted
|
|
4
|
+
the 'runs' in a series.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
# --- imports
|
|
8
|
+
from collections.abc import Sequence
|
|
9
|
+
from pandas import Series, concat
|
|
10
|
+
from matplotlib.pyplot import Axes
|
|
11
|
+
from matplotlib import patheffects as pe
|
|
12
|
+
|
|
13
|
+
from mgplot.settings import DataT
|
|
14
|
+
from mgplot.line_plot import line_plot
|
|
15
|
+
from mgplot.kw_type_checking import (
|
|
16
|
+
limit_kwargs,
|
|
17
|
+
ExpectedTypeDict,
|
|
18
|
+
validate_kwargs,
|
|
19
|
+
validate_expected,
|
|
20
|
+
report_kwargs,
|
|
21
|
+
)
|
|
22
|
+
from mgplot.line_plot import LP_KW_TYPES
|
|
23
|
+
from mgplot.utilities import constrain_data, check_clean_timeseries
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# --- constants
|
|
27
|
+
THRESHOLD = "threshold"
|
|
28
|
+
ROUND = "round"
|
|
29
|
+
HIGHLIGHT = "highlight"
|
|
30
|
+
DIRECTION = "direction"
|
|
31
|
+
|
|
32
|
+
RUN_KW_TYPES: ExpectedTypeDict = {
|
|
33
|
+
THRESHOLD: float,
|
|
34
|
+
ROUND: int,
|
|
35
|
+
HIGHLIGHT: (str, Sequence, (str,)), # colors for highlighting the runs
|
|
36
|
+
DIRECTION: str, # "up", "down" or "both"
|
|
37
|
+
}
|
|
38
|
+
validate_expected(RUN_KW_TYPES, "run_highlight_plot")
|
|
39
|
+
|
|
40
|
+
# --- functions
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _identify_runs(
|
|
44
|
+
series: Series,
|
|
45
|
+
threshold: float,
|
|
46
|
+
up: bool, # False means down
|
|
47
|
+
) -> tuple[Series, Series]:
|
|
48
|
+
"""Identify monotonic increasing/decreasing runs."""
|
|
49
|
+
|
|
50
|
+
diffed = series.diff()
|
|
51
|
+
change_points = concat(
|
|
52
|
+
[diffed[diffed.gt(threshold)], diffed[diffed.lt(-threshold)]]
|
|
53
|
+
).sort_index()
|
|
54
|
+
if series.index[0] not in change_points.index:
|
|
55
|
+
starting_point = Series([0], index=[series.index[0]])
|
|
56
|
+
change_points = concat([change_points, starting_point]).sort_index()
|
|
57
|
+
facing = change_points > 0 if up else change_points < 0
|
|
58
|
+
cycles = (facing & ~facing.shift().astype(bool)).cumsum()
|
|
59
|
+
return cycles[facing], change_points
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _plot_runs(
|
|
63
|
+
axes: Axes,
|
|
64
|
+
series: Series,
|
|
65
|
+
up: bool,
|
|
66
|
+
**kwargs,
|
|
67
|
+
) -> None:
|
|
68
|
+
"""Highlight the runs of a series."""
|
|
69
|
+
|
|
70
|
+
threshold = kwargs[THRESHOLD]
|
|
71
|
+
match kwargs.get(HIGHLIGHT): # make sure highlight is a color string
|
|
72
|
+
case str():
|
|
73
|
+
highlight = kwargs.get(HIGHLIGHT)
|
|
74
|
+
case Sequence():
|
|
75
|
+
highlight = kwargs[HIGHLIGHT][0] if up else kwargs[HIGHLIGHT][1]
|
|
76
|
+
case _:
|
|
77
|
+
raise ValueError(
|
|
78
|
+
f"Invalid type for highlight: {type(kwargs.get(HIGHLIGHT))}. "
|
|
79
|
+
"Expected str or Sequence."
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# highlight the runs
|
|
83
|
+
stretches, change_points = _identify_runs(series, threshold, up=up)
|
|
84
|
+
for k in range(1, stretches.max() + 1):
|
|
85
|
+
stretch = stretches[stretches == k]
|
|
86
|
+
axes.axvspan(
|
|
87
|
+
stretch.index.min(),
|
|
88
|
+
stretch.index.max(),
|
|
89
|
+
color=highlight,
|
|
90
|
+
zorder=-1,
|
|
91
|
+
)
|
|
92
|
+
space_above = series.max() - series[stretch.index].max()
|
|
93
|
+
space_below = series[stretch.index].min() - series.min()
|
|
94
|
+
y_pos, vert_align = (
|
|
95
|
+
(series.max(), "top")
|
|
96
|
+
if space_above > space_below
|
|
97
|
+
else (series.min(), "bottom")
|
|
98
|
+
)
|
|
99
|
+
text = axes.text(
|
|
100
|
+
x=stretch.index.min(),
|
|
101
|
+
y=y_pos,
|
|
102
|
+
s=(
|
|
103
|
+
change_points[stretch.index].sum().round(kwargs["round"]).astype(str)
|
|
104
|
+
+ " pp"
|
|
105
|
+
),
|
|
106
|
+
va=vert_align,
|
|
107
|
+
ha="left",
|
|
108
|
+
fontsize="small",
|
|
109
|
+
rotation=90,
|
|
110
|
+
)
|
|
111
|
+
text.set_path_effects([pe.withStroke(linewidth=5, foreground="w")])
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def run_plot(series: DataT, **kwargs) -> Axes:
|
|
115
|
+
"""Plot a series of percentage rates, highlighting the increasing runs.
|
|
116
|
+
|
|
117
|
+
Arguments
|
|
118
|
+
- series - ordered pandas Series of percentages, with PeriodIndex
|
|
119
|
+
- **kwargs
|
|
120
|
+
- threshold - float - used to ignore micro noise near zero
|
|
121
|
+
(for example, threshhold=0.01)
|
|
122
|
+
- round - int - rounding for highlight text
|
|
123
|
+
- highlight - str or Sequence[str] - color(s) for highlighting the
|
|
124
|
+
runs, two colors can be specified in a list if direction is "both"
|
|
125
|
+
- direction - str - whether the highlight is for an upward
|
|
126
|
+
or downward or both runs. Options are "up", "down" or "both".
|
|
127
|
+
- in addition the **kwargs for line_plot are accepted.
|
|
128
|
+
|
|
129
|
+
Return
|
|
130
|
+
- matplotlib Axes object"""
|
|
131
|
+
|
|
132
|
+
# --- sanity checks
|
|
133
|
+
series = check_clean_timeseries(series)
|
|
134
|
+
if not isinstance(series, Series):
|
|
135
|
+
raise TypeError("series must be a pandas Series for run_plot()")
|
|
136
|
+
series, kwargs = constrain_data(series, **kwargs)
|
|
137
|
+
|
|
138
|
+
# --- check the kwargs
|
|
139
|
+
report_kwargs(called_from="run_plot", **kwargs)
|
|
140
|
+
expected = RUN_KW_TYPES | LP_KW_TYPES
|
|
141
|
+
validate_kwargs(expected, "run_plot", **kwargs)
|
|
142
|
+
|
|
143
|
+
# --- default arguments - in **kwargs
|
|
144
|
+
kwargs[THRESHOLD] = kwargs.get(THRESHOLD, 0.1)
|
|
145
|
+
kwargs[ROUND] = kwargs.get(ROUND, 2)
|
|
146
|
+
direct = kwargs[DIRECTION] = kwargs.get(DIRECTION, "up")
|
|
147
|
+
kwargs[HIGHLIGHT], kwargs["color"] = (
|
|
148
|
+
(kwargs.get(HIGHLIGHT, "gold"), kwargs.get("color", "#dd0000"))
|
|
149
|
+
if direct == "up"
|
|
150
|
+
else (
|
|
151
|
+
(kwargs.get(HIGHLIGHT, "skyblue"), kwargs.get("color", "navy"))
|
|
152
|
+
if direct == "down"
|
|
153
|
+
else (
|
|
154
|
+
kwargs.get(HIGHLIGHT, ("gold", "skyblue")),
|
|
155
|
+
kwargs.get("color", "navy"),
|
|
156
|
+
)
|
|
157
|
+
)
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# defauls for line_plot
|
|
161
|
+
kwargs["width"] = kwargs.get("width", 2)
|
|
162
|
+
|
|
163
|
+
# plot the line
|
|
164
|
+
kwargs["drawstyle"] = kwargs.get("drawstyle", "steps-post")
|
|
165
|
+
lp_kwargs = limit_kwargs(LP_KW_TYPES, **kwargs)
|
|
166
|
+
axes = line_plot(series, **lp_kwargs)
|
|
167
|
+
|
|
168
|
+
# plot the runs
|
|
169
|
+
match kwargs[DIRECTION]:
|
|
170
|
+
case "up":
|
|
171
|
+
_plot_runs(axes, series, up=True, **kwargs)
|
|
172
|
+
case "down":
|
|
173
|
+
_plot_runs(axes, series, up=False, **kwargs)
|
|
174
|
+
case "both":
|
|
175
|
+
_plot_runs(axes, series, up=True, **kwargs)
|
|
176
|
+
_plot_runs(axes, series, up=False, **kwargs)
|
|
177
|
+
case _:
|
|
178
|
+
raise ValueError(
|
|
179
|
+
f"Invalid value for direction: {kwargs[DIRECTION]}. "
|
|
180
|
+
"Expected 'up', 'down', or 'both'."
|
|
181
|
+
)
|
|
182
|
+
return axes
|