tesorotools-python 0.0.7__py2.py3-none-any.whl → 0.0.8__py2.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.
- tesorotools/__init__.py +0 -0
- tesorotools/artists/__init__.py +5 -0
- tesorotools/artists/barh_plot.py +310 -0
- tesorotools/artists/line_plot.py +114 -0
- tesorotools/artists/table.py +199 -0
- tesorotools/artists/type_curve.py +216 -0
- tesorotools/assets/README.md +5 -0
- tesorotools/assets/fonts/README.md +1 -0
- tesorotools/assets/plots.yaml +43 -0
- tesorotools/assets/tesoro.mplstyle +21 -0
- tesorotools/convert.py +93 -0
- tesorotools/data_sources/README.md +14 -0
- tesorotools/data_sources/__init__.py +0 -0
- tesorotools/data_sources/debug.py +26 -0
- tesorotools/data_sources/lseg.py +117 -0
- tesorotools/database/__init__.py +0 -0
- tesorotools/database/push.py +70 -0
- tesorotools/dependencies/__init__.py +0 -0
- tesorotools/dependencies/functions.py +11 -0
- tesorotools/dependencies/node.py +34 -0
- tesorotools/dependencies/resolution.py +118 -0
- tesorotools/main.py +37 -0
- tesorotools/offsets/__init__.py +0 -0
- tesorotools/offsets/offsets.py +439 -0
- tesorotools/offsets/outliers.py +15 -0
- tesorotools/render/__init__.py +11 -0
- tesorotools/render/content/__init__.py +0 -0
- tesorotools/render/content/content.py +17 -0
- tesorotools/render/content/images.py +147 -0
- tesorotools/render/content/section.py +53 -0
- tesorotools/render/content/table.py +284 -0
- tesorotools/render/headline.py +40 -0
- tesorotools/render/introduction.py +49 -0
- tesorotools/render/report.py +29 -0
- tesorotools/utils/__init__.py +0 -0
- tesorotools/utils/config.py +35 -0
- tesorotools/utils/globals.py +14 -0
- tesorotools/utils/matplotlib.py +38 -0
- tesorotools/utils/series.py +40 -0
- tesorotools/utils/template.py +126 -0
- {tesorotools_python-0.0.7.dist-info → tesorotools_python-0.0.8.dist-info}/METADATA +1 -1
- tesorotools_python-0.0.8.dist-info/RECORD +43 -0
- tesorotools_python-0.0.7.dist-info/RECORD +0 -3
- {tesorotools_python-0.0.7.dist-info → tesorotools_python-0.0.8.dist-info}/WHEEL +0 -0
tesorotools/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import matplotlib.pyplot as plt
|
|
6
|
+
import pandas as pd
|
|
7
|
+
from matplotlib.container import BarContainer
|
|
8
|
+
from matplotlib.ticker import FuncFormatter
|
|
9
|
+
|
|
10
|
+
from tesorotools.offsets.offsets import Difference, FloatingOffset, Stat
|
|
11
|
+
from tesorotools.offsets.outliers import flag_outliers
|
|
12
|
+
from tesorotools.utils.globals import DEBUG
|
|
13
|
+
from tesorotools.utils.matplotlib import (
|
|
14
|
+
PLOT_CONFIG,
|
|
15
|
+
format_annotation,
|
|
16
|
+
load_fonts,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
BARH_CONFIG: dict[str, Any] = PLOT_CONFIG["barh"]
|
|
20
|
+
AX_CONFIG: dict[str, Any] = PLOT_CONFIG["ax"]
|
|
21
|
+
FIG_CONFIG: dict[str, Any] = PLOT_CONFIG["figure"]
|
|
22
|
+
|
|
23
|
+
load_fonts()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Column(Enum):
|
|
27
|
+
VALUE = "value"
|
|
28
|
+
AXIS = "axis"
|
|
29
|
+
DEVIATION = "deviation"
|
|
30
|
+
COLOR = "color"
|
|
31
|
+
ALPHA = "alpha"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _style_spines(
|
|
35
|
+
ax: plt.Axes,
|
|
36
|
+
decimals: int,
|
|
37
|
+
units: str,
|
|
38
|
+
*,
|
|
39
|
+
color: str,
|
|
40
|
+
linewidth: str,
|
|
41
|
+
):
|
|
42
|
+
ax.grid(visible=True, axis="x")
|
|
43
|
+
for spine in ax.spines.values():
|
|
44
|
+
spine.set_color(color)
|
|
45
|
+
spine.set_linewidth(linewidth)
|
|
46
|
+
ax.xaxis.set_major_formatter(
|
|
47
|
+
FuncFormatter(lambda x, _: format_annotation(x, decimals, units))
|
|
48
|
+
)
|
|
49
|
+
ax.tick_params(axis="both", which="major")
|
|
50
|
+
for tick in ax.get_xticklines():
|
|
51
|
+
tick.set_markeredgecolor(color)
|
|
52
|
+
for tick in ax.get_yticklines():
|
|
53
|
+
tick.set_markeredgecolor(color)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _style_baseline(ax: plt.Axes, **baseline_config):
|
|
57
|
+
color: str = baseline_config["color"]
|
|
58
|
+
left_lim, right_lim = ax.get_xlim()
|
|
59
|
+
ax.set_xlim(left=min(0, left_lim), right=max(0, right_lim))
|
|
60
|
+
left_lim, right_lim = ax.get_xlim()
|
|
61
|
+
if left_lim == 0:
|
|
62
|
+
ax.spines["left"].set_edgecolor(color)
|
|
63
|
+
elif right_lim == 0:
|
|
64
|
+
ax.spines["right"].set_edgecolor(color)
|
|
65
|
+
else:
|
|
66
|
+
ax.axvline(x=0, **baseline_config)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _collect_series(
|
|
70
|
+
blocks: dict[str, Any] | None, series: dict[str, str] | None
|
|
71
|
+
) -> dict[str, str]:
|
|
72
|
+
if series is None and blocks is None:
|
|
73
|
+
raise ValueError("blocks and series cannot be both missing")
|
|
74
|
+
if series is None and blocks is not None:
|
|
75
|
+
return _collect_block_series(blocks)
|
|
76
|
+
else:
|
|
77
|
+
return series
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _collect_block_series(blocks: dict[str, Any]) -> dict[str, str]:
|
|
81
|
+
series = {}
|
|
82
|
+
for _, block_cfg in blocks.items():
|
|
83
|
+
series = series | block_cfg["series"]
|
|
84
|
+
return series
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _infer_colors(
|
|
88
|
+
value_series: pd.Series, blocks: dict[str, Any] | None
|
|
89
|
+
) -> pd.Series:
|
|
90
|
+
color_series: pd.Series = pd.Series(
|
|
91
|
+
index=value_series.index, name=Column.COLOR.value, dtype=str
|
|
92
|
+
)
|
|
93
|
+
if blocks is not None:
|
|
94
|
+
for idx, block_cfg in enumerate(blocks.values()):
|
|
95
|
+
block_series: dict[str, str] = block_cfg["series"]
|
|
96
|
+
color_series.loc[block_series.keys()] = f"C{idx}"
|
|
97
|
+
else:
|
|
98
|
+
color_series[value_series >= 0] = "C0"
|
|
99
|
+
color_series[value_series < 0] = "C1"
|
|
100
|
+
return color_series
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _highlight_series(
|
|
104
|
+
alias: dict[str, str], value_series: pd.Series
|
|
105
|
+
) -> pd.Series:
|
|
106
|
+
alpha_series: pd.Series = pd.Series(
|
|
107
|
+
index=value_series.index, name=Column.ALPHA.value
|
|
108
|
+
)
|
|
109
|
+
alpha_series.loc[:] = 1
|
|
110
|
+
high_series = [k for k, v in alias.items() if v.endswith("*")]
|
|
111
|
+
alpha_series.loc[high_series] = BARH_CONFIG["highlight_factor"]
|
|
112
|
+
return alpha_series
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _format_yaxis(
|
|
116
|
+
alias: dict[str, str],
|
|
117
|
+
axis_format: dict[str, Any],
|
|
118
|
+
value_series: pd.Series,
|
|
119
|
+
axis_series: pd.Series | None,
|
|
120
|
+
) -> pd.Series:
|
|
121
|
+
# format y axis ticker labels
|
|
122
|
+
renamer = {_: label.replace("*", "") for _, label in alias.items()}
|
|
123
|
+
value_series = value_series.rename(renamer)
|
|
124
|
+
if axis_format is not None:
|
|
125
|
+
decimals: int = axis_format["decimals"]
|
|
126
|
+
units: str = axis_format["units"]
|
|
127
|
+
axis_series: pd.Series = axis_series.rename(renamer).apply(
|
|
128
|
+
lambda x: format_annotation(x, decimals, units)
|
|
129
|
+
)
|
|
130
|
+
value_series = value_series.rename(
|
|
131
|
+
lambda x: f"{x} ({axis_series.loc[x]})"
|
|
132
|
+
)
|
|
133
|
+
return value_series
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _annotate(
|
|
137
|
+
fig: plt.Figure,
|
|
138
|
+
ax: plt.Axes,
|
|
139
|
+
bar_container: BarContainer,
|
|
140
|
+
*,
|
|
141
|
+
decimals: int,
|
|
142
|
+
units: str,
|
|
143
|
+
):
|
|
144
|
+
# annotate
|
|
145
|
+
labels = ax.bar_label(
|
|
146
|
+
container=bar_container,
|
|
147
|
+
fmt=lambda x: format_annotation(x, decimals, units),
|
|
148
|
+
padding=BARH_CONFIG["padding"],
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# rescale
|
|
152
|
+
fig.canvas.draw_idle()
|
|
153
|
+
for label in labels:
|
|
154
|
+
bbox = label.get_window_extent()
|
|
155
|
+
bbox_data = bbox.transformed(ax.transData.inverted())
|
|
156
|
+
ax.update_datalim(bbox_data.corners())
|
|
157
|
+
ax.autoscale_view()
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _plot_barh_chart(
|
|
161
|
+
out_file: Path,
|
|
162
|
+
standard_dict: dict[Column, pd.Series | None],
|
|
163
|
+
alias: dict[str, str],
|
|
164
|
+
sorted: bool,
|
|
165
|
+
format: dict,
|
|
166
|
+
annot_format: dict,
|
|
167
|
+
axis_format: dict | None = None,
|
|
168
|
+
blocks: dict | None = None,
|
|
169
|
+
**kwargs,
|
|
170
|
+
):
|
|
171
|
+
# infer colors
|
|
172
|
+
value_series: pd.Series = standard_dict[Column.VALUE]
|
|
173
|
+
color_series: pd.Series = _infer_colors(value_series, blocks)
|
|
174
|
+
alpha_series: pd.Series = _highlight_series(alias, value_series)
|
|
175
|
+
|
|
176
|
+
# format y axis ticker labels
|
|
177
|
+
axis_series = standard_dict[Column.AXIS]
|
|
178
|
+
value_series = _format_yaxis(alias, axis_format, value_series, axis_series)
|
|
179
|
+
color_series.index = value_series.index
|
|
180
|
+
alpha_series.index = value_series.index
|
|
181
|
+
|
|
182
|
+
data: pd.DataFrame = pd.concat(
|
|
183
|
+
[value_series, color_series, alpha_series], axis=1
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
# sort if required
|
|
187
|
+
if sorted:
|
|
188
|
+
data = data.sort_values(by=Column.VALUE.value)
|
|
189
|
+
|
|
190
|
+
# plot
|
|
191
|
+
fig = plt.figure(**FIG_CONFIG)
|
|
192
|
+
ax = fig.add_subplot()
|
|
193
|
+
|
|
194
|
+
bar_container: BarContainer = ax.barh(
|
|
195
|
+
y=data.index,
|
|
196
|
+
width=data[Column.VALUE.value],
|
|
197
|
+
color=data[Column.COLOR.value],
|
|
198
|
+
)
|
|
199
|
+
for bar, alpha in zip(bar_container, data[Column.ALPHA.value]):
|
|
200
|
+
bar.set_alpha(alpha)
|
|
201
|
+
|
|
202
|
+
_annotate(fig, ax, bar_container, **annot_format)
|
|
203
|
+
_style_spines(ax, **format, **AX_CONFIG["spines"])
|
|
204
|
+
_style_baseline(ax, **AX_CONFIG["baseline"])
|
|
205
|
+
|
|
206
|
+
fig.savefig(out_file)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _normalize_from_flash(
|
|
210
|
+
flash: pd.DataFrame,
|
|
211
|
+
axis: bool,
|
|
212
|
+
*,
|
|
213
|
+
date: str | pd.Timestamp | None,
|
|
214
|
+
offset: str,
|
|
215
|
+
difference: str,
|
|
216
|
+
deviations: bool,
|
|
217
|
+
units_bar: str,
|
|
218
|
+
units_axis: str,
|
|
219
|
+
) -> dict[Column, pd.Series | None]:
|
|
220
|
+
|
|
221
|
+
# format parameters
|
|
222
|
+
date: pd.Timestamp = (
|
|
223
|
+
flash.index.get_level_values(level=0).max()
|
|
224
|
+
if date is None
|
|
225
|
+
else pd.to_datetime(date)
|
|
226
|
+
)
|
|
227
|
+
offset: FloatingOffset = FloatingOffset(offset)
|
|
228
|
+
difference: Difference = Difference(difference)
|
|
229
|
+
|
|
230
|
+
# value column
|
|
231
|
+
values_series: pd.Series = flash.loc[
|
|
232
|
+
(date, offset.value, difference.value, Stat.VALUE.value),
|
|
233
|
+
:,
|
|
234
|
+
].copy()
|
|
235
|
+
values_series.name = Column.VALUE.value
|
|
236
|
+
values_series = (
|
|
237
|
+
values_series * 100 if difference is Difference.REL else values_series
|
|
238
|
+
)
|
|
239
|
+
values_series = (
|
|
240
|
+
values_series * 100
|
|
241
|
+
if (difference is Difference.ABS and units_bar == "p.b.")
|
|
242
|
+
else values_series
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
# axis column
|
|
246
|
+
if axis:
|
|
247
|
+
axis_series: pd.Series = flash.loc[
|
|
248
|
+
(
|
|
249
|
+
date,
|
|
250
|
+
FloatingOffset.NO.value,
|
|
251
|
+
Difference.NO.value,
|
|
252
|
+
Stat.VALUE.value,
|
|
253
|
+
),
|
|
254
|
+
:,
|
|
255
|
+
].copy()
|
|
256
|
+
axis_series = (
|
|
257
|
+
axis_series * 100
|
|
258
|
+
if (difference is Difference.ABS and units_axis == "p.b.")
|
|
259
|
+
else axis_series
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
axis_series.name = Column.AXIS.value
|
|
263
|
+
else:
|
|
264
|
+
axis_series = None
|
|
265
|
+
|
|
266
|
+
# deviations column
|
|
267
|
+
if deviations:
|
|
268
|
+
deviations_df: pd.DataFrame = flash.loc[
|
|
269
|
+
(
|
|
270
|
+
date,
|
|
271
|
+
offset.value,
|
|
272
|
+
difference.value,
|
|
273
|
+
[Stat.VALUE.value, Stat.ROLL_AVG.value, Stat.ROLL_STD._value_],
|
|
274
|
+
),
|
|
275
|
+
:,
|
|
276
|
+
].T.copy()
|
|
277
|
+
deviations_df.columns = deviations_df.columns.get_level_values(level=-1)
|
|
278
|
+
deviations_df.columns.name = None
|
|
279
|
+
deviations_series: pd.Series = flag_outliers(deviations_df)
|
|
280
|
+
deviations_series.name = Column.DEVIATION.value
|
|
281
|
+
else:
|
|
282
|
+
deviations_series = None
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
Column.VALUE: values_series,
|
|
286
|
+
Column.AXIS: axis_series,
|
|
287
|
+
Column.DEVIATION: deviations_series,
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def plot_barh_charts_from_flash(
|
|
292
|
+
flash: pd.DataFrame, config_dicts: dict[str, dict]
|
|
293
|
+
):
|
|
294
|
+
for name, config in config_dicts.items():
|
|
295
|
+
blocks: dict[str, Any] = config.get("blocks", None)
|
|
296
|
+
series: dict[str, str] | None = config.get("series", None)
|
|
297
|
+
alias = _collect_series(blocks, series)
|
|
298
|
+
trimmed_flash: pd.DataFrame = flash.loc[:, alias.keys()]
|
|
299
|
+
flash_config: dict[str, Any] = config["flash"]
|
|
300
|
+
axis_format: dict[str, Any] = config.get("axis_format", None)
|
|
301
|
+
axis = axis_format is not None
|
|
302
|
+
standard_dict: dict[Column, pd.Series | None] = _normalize_from_flash(
|
|
303
|
+
trimmed_flash,
|
|
304
|
+
axis,
|
|
305
|
+
**flash_config,
|
|
306
|
+
units_bar=config["format"]["units"],
|
|
307
|
+
units_axis=config.get("axis_format", {"units": ""})["units"],
|
|
308
|
+
)
|
|
309
|
+
out_file = Path(DEBUG / "barh" / f"{name}.png")
|
|
310
|
+
_plot_barh_chart(out_file, standard_dict, alias, **config)
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import locale
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import matplotlib.pyplot as plt
|
|
6
|
+
import pandas as pd
|
|
7
|
+
from matplotlib.ticker import FuncFormatter
|
|
8
|
+
|
|
9
|
+
locale.setlocale(locale.LC_ALL, "")
|
|
10
|
+
|
|
11
|
+
from tesorotools.utils.globals import DEBUG
|
|
12
|
+
from tesorotools.utils.matplotlib import (
|
|
13
|
+
PLOT_CONFIG,
|
|
14
|
+
format_annotation,
|
|
15
|
+
load_fonts,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
load_fonts()
|
|
19
|
+
|
|
20
|
+
LINE_PLOT_CONFIG: dict[str, Any] = PLOT_CONFIG["line"]
|
|
21
|
+
AX_CONFIG: dict[str, Any] = PLOT_CONFIG["ax"]
|
|
22
|
+
FIG_CONFIG: dict[str, Any] = PLOT_CONFIG["figure"]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _style_spines(
|
|
26
|
+
ax: plt.Axes,
|
|
27
|
+
decimals: int,
|
|
28
|
+
units: str,
|
|
29
|
+
*,
|
|
30
|
+
color: str,
|
|
31
|
+
linewidth: str,
|
|
32
|
+
):
|
|
33
|
+
ax.grid(visible=True, axis="y")
|
|
34
|
+
for spine in ax.spines.values():
|
|
35
|
+
spine.set_color(color)
|
|
36
|
+
spine.set_linewidth(linewidth)
|
|
37
|
+
ax.yaxis.tick_right()
|
|
38
|
+
ax.yaxis.set_major_formatter(
|
|
39
|
+
FuncFormatter(lambda y, _: format_annotation(y, decimals, units))
|
|
40
|
+
)
|
|
41
|
+
ax.set_xlabel("")
|
|
42
|
+
|
|
43
|
+
ax.tick_params(which="minor", size=0, width=0)
|
|
44
|
+
ax.tick_params(axis="both", which="major")
|
|
45
|
+
for tick in ax.get_xticklines():
|
|
46
|
+
tick.set_markeredgecolor(color)
|
|
47
|
+
for tick in ax.get_yticklines():
|
|
48
|
+
tick.set_markeredgecolor(color)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _style_baseline(ax: plt.Axes, reference: float = 0, **baseline_config):
|
|
52
|
+
color: str = baseline_config["color"]
|
|
53
|
+
bottom_lim, top_lim = ax.get_ylim()
|
|
54
|
+
ax.set_ylim(bottom=min(reference, bottom_lim), top=max(reference, top_lim))
|
|
55
|
+
bottom_lim, top_lim = ax.get_ylim()
|
|
56
|
+
if bottom_lim == reference:
|
|
57
|
+
ax.spines["bottom"].set_edgecolor(color)
|
|
58
|
+
elif top_lim == reference:
|
|
59
|
+
ax.spines["top"].set_edgecolor(color)
|
|
60
|
+
else:
|
|
61
|
+
ax.axhline(y=reference, **baseline_config)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def plot_line_chart(
|
|
65
|
+
out_name: Path,
|
|
66
|
+
data: pd.DataFrame,
|
|
67
|
+
*,
|
|
68
|
+
base_100: bool,
|
|
69
|
+
annotate: bool,
|
|
70
|
+
format: dict[str, Any],
|
|
71
|
+
**kwargs,
|
|
72
|
+
):
|
|
73
|
+
if base_100:
|
|
74
|
+
data = data / data.iloc[0, :] * 100
|
|
75
|
+
if format["units"] == "p.b.":
|
|
76
|
+
data = data * 100
|
|
77
|
+
fig = plt.figure(**FIG_CONFIG)
|
|
78
|
+
ax = fig.add_subplot()
|
|
79
|
+
data.plot(ax=ax)
|
|
80
|
+
if annotate:
|
|
81
|
+
pass
|
|
82
|
+
|
|
83
|
+
reference = 100 if base_100 else 0
|
|
84
|
+
_style_spines(ax, **format, **AX_CONFIG["spines"])
|
|
85
|
+
_style_baseline(ax, reference, **AX_CONFIG["baseline"])
|
|
86
|
+
ax.legend(
|
|
87
|
+
loc="upper center",
|
|
88
|
+
bbox_to_anchor=(0.5, LINE_PLOT_CONFIG["legend_sep"]),
|
|
89
|
+
ncol=(
|
|
90
|
+
kwargs["legend"]["ncol"]
|
|
91
|
+
if kwargs is not None and kwargs.get("legend", None) is not None
|
|
92
|
+
else LINE_PLOT_CONFIG["ncol"]
|
|
93
|
+
),
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
fig.savefig(out_name)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def plot_line_charts(data: pd.DataFrame, config_dicts: dict[str, Any]):
|
|
100
|
+
for name, config in config_dicts.items():
|
|
101
|
+
start_date: pd.Timestamp = pd.to_datetime(config["start_date"])
|
|
102
|
+
end_date_str: str | None = config["end_date"]
|
|
103
|
+
end_date: pd.Timestamp = (
|
|
104
|
+
data.index.max()
|
|
105
|
+
if end_date_str is None
|
|
106
|
+
else pd.to_datetime(end_date_str)
|
|
107
|
+
)
|
|
108
|
+
series: dict[str, str] = config["series"]
|
|
109
|
+
trimmed_data: pd.DataFrame = data.loc[
|
|
110
|
+
slice(start_date, end_date), series.keys()
|
|
111
|
+
]
|
|
112
|
+
trimmed_data = trimmed_data.rename(columns=series)
|
|
113
|
+
out_name: Path = DEBUG / "line" / f"{name}.png"
|
|
114
|
+
plot_line_chart(out_name, trimmed_data, **config)
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
from math import floor
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
import pandas as pd
|
|
5
|
+
|
|
6
|
+
from tesorotools.dependencies.resolution import collect_series
|
|
7
|
+
from tesorotools.offsets.outliers import flag_outliers
|
|
8
|
+
from tesorotools.utils.globals import DEBUG
|
|
9
|
+
from tesorotools.utils.matplotlib import format_annotation, is_zero
|
|
10
|
+
|
|
11
|
+
# this file is by far the worst and most spaghettified, must be rewritten
|
|
12
|
+
|
|
13
|
+
# to global config
|
|
14
|
+
GOOD: str = "00c800"
|
|
15
|
+
BAD: str = "c80000"
|
|
16
|
+
THRESHOLD: float = 1
|
|
17
|
+
SHADE_LEVELS = 2
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _shade_intensity(
|
|
21
|
+
ratio: float, shade_levels: int = 2, continuous: bool = False
|
|
22
|
+
) -> str:
|
|
23
|
+
# intensity may vary from 150 (highest) to 255 (lowest), a grand difference of 105
|
|
24
|
+
# there are SHADE_LEVELS levels, so increments will be of 105/SHADE_LEVELS
|
|
25
|
+
corrected_ratio: float = min(ratio, shade_levels)
|
|
26
|
+
corrected_ratio: float = (
|
|
27
|
+
floor(corrected_ratio) if not continuous else corrected_ratio
|
|
28
|
+
)
|
|
29
|
+
increment: float = (corrected_ratio - 1) * (105 / shade_levels)
|
|
30
|
+
intensity: float = 255 - increment
|
|
31
|
+
intensity_hex: str = f"{int(intensity):x}"
|
|
32
|
+
return intensity_hex
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _generate_column(
|
|
36
|
+
column_data: pd.Series,
|
|
37
|
+
column_cfg: dict[str, Any],
|
|
38
|
+
outliers_flags: pd.Series | None = None,
|
|
39
|
+
):
|
|
40
|
+
# TODO: factor out
|
|
41
|
+
# data
|
|
42
|
+
if column_cfg["show_units_in_title"]:
|
|
43
|
+
column_data.name = f"{column_cfg["name"]} ({column_cfg["unit"]})"
|
|
44
|
+
else:
|
|
45
|
+
column_data.name = column_cfg["name"]
|
|
46
|
+
column_cfg["formatted_name"] = column_data.name
|
|
47
|
+
|
|
48
|
+
unit = (
|
|
49
|
+
column_cfg["unit"]
|
|
50
|
+
if column_cfg["show_units_in_cell"] and column_cfg["unit"] is not None
|
|
51
|
+
else ""
|
|
52
|
+
)
|
|
53
|
+
scaled_data: pd.Series = column_data * column_cfg["scale"]
|
|
54
|
+
formatted_data = scaled_data.apply(
|
|
55
|
+
lambda x: format_annotation(
|
|
56
|
+
x, decimals=column_cfg["decimals"], units=unit
|
|
57
|
+
)
|
|
58
|
+
)
|
|
59
|
+
zeros: pd.Series = scaled_data.apply(
|
|
60
|
+
lambda x: is_zero(x, decimals=column_cfg["decimals"])
|
|
61
|
+
)
|
|
62
|
+
positives: pd.Series = scaled_data > 0
|
|
63
|
+
negatives: pd.Series = scaled_data < 0
|
|
64
|
+
|
|
65
|
+
# colors
|
|
66
|
+
colors_cfg: bool = column_cfg["colors"]
|
|
67
|
+
color_data: pd.Series = pd.Series(
|
|
68
|
+
index=formatted_data.index, name=column_data.name, dtype=str
|
|
69
|
+
)
|
|
70
|
+
if colors_cfg:
|
|
71
|
+
positive_good: bool = column_cfg["positive_good"]
|
|
72
|
+
color_data[positives] = GOOD if positive_good else BAD
|
|
73
|
+
color_data.loc[negatives] = BAD if positive_good else GOOD
|
|
74
|
+
color_data.loc[zeros.values] = pd.NA
|
|
75
|
+
|
|
76
|
+
# shades
|
|
77
|
+
shade_data: pd.Series = pd.Series(
|
|
78
|
+
index=formatted_data.index, name=column_data.name, dtype=str
|
|
79
|
+
)
|
|
80
|
+
if outliers_flags is not None:
|
|
81
|
+
thresholds: pd.Series = abs(outliers_flags / THRESHOLD)
|
|
82
|
+
intensities: pd.Series = thresholds.apply(
|
|
83
|
+
lambda x: _shade_intensity(x, SHADE_LEVELS)
|
|
84
|
+
)
|
|
85
|
+
shade_data[(thresholds >= 1) & (outliers_flags > 0)] = intensities[
|
|
86
|
+
(thresholds >= 1) & (outliers_flags > 0)
|
|
87
|
+
].apply(lambda x: f"00{x}00" if positive_good else f"{x}0000")
|
|
88
|
+
shade_data[(thresholds >= 1) & (outliers_flags < 0)] = intensities[
|
|
89
|
+
(thresholds >= 1) & (outliers_flags < 0)
|
|
90
|
+
].apply(lambda x: f"{x}0000" if positive_good else f"00{x}00")
|
|
91
|
+
|
|
92
|
+
return formatted_data, color_data, shade_data
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _generate_block(block_data: pd.DataFrame, block_cfg: dict[str, Any]):
|
|
96
|
+
columns: dict[str, Any] = block_cfg["columns"]
|
|
97
|
+
formatted_columns: list[pd.Series] = []
|
|
98
|
+
color_columns: list[pd.Series] = []
|
|
99
|
+
shade_columns: list[pd.Series] = []
|
|
100
|
+
sort_idx: pd.Index | None = None
|
|
101
|
+
for column_name, column_cfg in columns.items():
|
|
102
|
+
last_date: pd.Timestamp = block_data.index.get_level_values(
|
|
103
|
+
level=0
|
|
104
|
+
).max()
|
|
105
|
+
block_data = block_data.rename(columns=block_cfg["series"])
|
|
106
|
+
offset: str = column_cfg["offset"]
|
|
107
|
+
difference: str = column_cfg["difference"]
|
|
108
|
+
stat: str = column_cfg["stat"]
|
|
109
|
+
outliers: bool = column_cfg["outliers"]
|
|
110
|
+
|
|
111
|
+
stat_data = block_data.loc[
|
|
112
|
+
(last_date, offset, difference, slice(None)), :
|
|
113
|
+
]
|
|
114
|
+
stat_data.index = stat_data.index.get_level_values(level=-1)
|
|
115
|
+
|
|
116
|
+
outliers_flags: pd.Series | None = None
|
|
117
|
+
if outliers:
|
|
118
|
+
outliers_flags = flag_outliers(stat_data.T)
|
|
119
|
+
|
|
120
|
+
column_data: pd.Series = stat_data.loc[stat, :]
|
|
121
|
+
# sort capability
|
|
122
|
+
sort: str = block_cfg.get("sort", None)
|
|
123
|
+
if sort is not None and column_name == sort:
|
|
124
|
+
column_data: pd.Series = column_data.sort_values(ascending=False)
|
|
125
|
+
sort_idx = column_data.index
|
|
126
|
+
formatted_column, color_column, shade_column = _generate_column(
|
|
127
|
+
column_data, column_cfg, outliers_flags
|
|
128
|
+
)
|
|
129
|
+
formatted_columns.append(formatted_column)
|
|
130
|
+
color_columns.append(color_column)
|
|
131
|
+
shade_columns.append(shade_column)
|
|
132
|
+
|
|
133
|
+
if sort_idx is not None:
|
|
134
|
+
formatted_columns = [s.reindex(sort_idx) for s in formatted_columns]
|
|
135
|
+
color_columns = [s.reindex(sort_idx) for s in color_columns]
|
|
136
|
+
shade_columns = [s.reindex(sort_idx) for s in shade_columns]
|
|
137
|
+
|
|
138
|
+
formatted_block: pd.DataFrame = pd.concat(formatted_columns, axis=1)
|
|
139
|
+
color_block: pd.DataFrame = pd.concat(color_columns, axis=1)
|
|
140
|
+
shade_block: pd.DataFrame = pd.concat(shade_columns, axis=1)
|
|
141
|
+
|
|
142
|
+
formatted_block.columns.name = block_cfg["title"]
|
|
143
|
+
color_block.columns.name = block_cfg["title"]
|
|
144
|
+
shade_block.columns.name = block_cfg["title"]
|
|
145
|
+
return formatted_block, color_block, shade_block
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def generate_table(table_data: pd.DataFrame, table_cfg: dict[str, Any]):
|
|
149
|
+
blocks: dict[str, dict] = table_cfg["blocks"]
|
|
150
|
+
formatted_blocks: list[pd.DataFrame] = []
|
|
151
|
+
color_blocks: list[pd.DataFrame] = []
|
|
152
|
+
shade_blocks: list[pd.DataFrame] = []
|
|
153
|
+
axis = 0 if table_cfg["axis"] == "vertical" else 1
|
|
154
|
+
# sorting capabilities
|
|
155
|
+
for block_name, block_cfg in blocks.items():
|
|
156
|
+
series_dict: dict[str, str] = block_cfg["series"]
|
|
157
|
+
block_data: pd.DataFrame = table_data.loc[:, series_dict.keys()]
|
|
158
|
+
formatted_block, color_block, shade_block = _generate_block(
|
|
159
|
+
block_data, block_cfg
|
|
160
|
+
)
|
|
161
|
+
formatted_blocks.append(formatted_block)
|
|
162
|
+
color_blocks.append(color_block)
|
|
163
|
+
shade_blocks.append(shade_block)
|
|
164
|
+
formatted_table = pd.concat(
|
|
165
|
+
formatted_blocks,
|
|
166
|
+
axis=axis,
|
|
167
|
+
keys=[
|
|
168
|
+
formatted_block.columns.name for formatted_block in formatted_blocks
|
|
169
|
+
],
|
|
170
|
+
)
|
|
171
|
+
color_table = pd.concat(
|
|
172
|
+
color_blocks,
|
|
173
|
+
axis=axis,
|
|
174
|
+
keys=[
|
|
175
|
+
formatted_block.columns.name for formatted_block in formatted_blocks
|
|
176
|
+
],
|
|
177
|
+
)
|
|
178
|
+
shade_table = pd.concat(
|
|
179
|
+
shade_blocks,
|
|
180
|
+
axis=axis,
|
|
181
|
+
keys=[
|
|
182
|
+
formatted_block.columns.name for formatted_block in formatted_blocks
|
|
183
|
+
],
|
|
184
|
+
)
|
|
185
|
+
return formatted_table, color_table, shade_table
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def generate_tables_from_flash(
|
|
189
|
+
flash: pd.DataFrame, config_dicts: dict[str, dict]
|
|
190
|
+
):
|
|
191
|
+
for table_name, table_cfg in config_dicts.items():
|
|
192
|
+
series: list[str] = list(collect_series(table_cfg))
|
|
193
|
+
table_data: pd.DataFrame = flash.loc[:, series]
|
|
194
|
+
formatted_table, color_table, shade_table = generate_table(
|
|
195
|
+
table_data, table_cfg
|
|
196
|
+
)
|
|
197
|
+
formatted_table.to_feather(DEBUG / "table" / f"{table_name}.feather")
|
|
198
|
+
color_table.to_feather(DEBUG / "table" / f"{table_name}_color.feather")
|
|
199
|
+
shade_table.to_feather(DEBUG / "table" / f"{table_name}_shade.feather")
|