Skip to content

Commit e23b7fe

Browse files
committed
moves things that didn't work
1 parent 5adf548 commit e23b7fe

2 files changed

Lines changed: 298 additions & 0 deletions

File tree

03_things_that_didnt_work.qmd

Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
---
2+
title: "Things that didn't work"
3+
format:
4+
html:
5+
toc: true
6+
number-sections: false
7+
code-tools: true
8+
anchor-sections: true
9+
jupyter: python3
10+
editor:
11+
render-on-save: true
12+
---
13+
14+
This section is a non-exhaustive list problems I wasn't able to solve with `plotnine`.
15+
16+
Note that this tutorial was made with `plotnine` version `0.15.0`. I fully expect that this appendix will likely very quickly become irrelevant with the many anticipated improvements that are coming to `plotnine` in the near future.
17+
18+
# Setup {.hidden .unlisted .unnumbered}
19+
20+
## Parameters
21+
22+
```{python}
23+
from pyprojroot import here
24+
import mpl_fontkit as fk
25+
from brand_yml import Brand
26+
```
27+
```{python}
28+
# | tags: [parameters]
29+
LABOUR_DATA_FILE = here() / "data" / "14100355.csv"
30+
FIGURE_THEME_SIZE = (9, 5)
31+
FILTER_YEAR = (2018, 2025)
32+
BRAND = Brand.from_yaml(here())
33+
FONT_PRIMARY = BRAND.typography.base.model_dump()["family"]
34+
FONT_SECONDARY = "Lato"
35+
fk.install(FONT_PRIMARY)
36+
fk.install(FONT_SECONDARY)
37+
COLOR_BACKGROUND = BRAND.color.background
38+
```
39+
40+
## Libraries
41+
42+
```{python}
43+
from labourcan.data_processing import (
44+
read_labourcan,
45+
calculate_centered_rank,
46+
cut_pdiff,
47+
DEFAULT_CUTS
48+
)
49+
import polars as pl
50+
import polars.selectors as cs
51+
from mizani.bounds import squish
52+
import mizani.labels as ml
53+
import mizani.breaks as mb
54+
import textwrap
55+
from great_tables import GT, md, html
56+
from plotnine import *
57+
from IPython.display import display, Markdown
58+
import matplotlib.pyplot as plt
59+
import re
60+
```
61+
62+
63+
```{python}
64+
labour = read_labourcan(LABOUR_DATA_FILE)
65+
66+
# Remove Aggregated Rows
67+
labour_filtered = labour.filter(
68+
~pl.col("Industry").is_in(
69+
[
70+
"Total employed, all industries",
71+
"Goods-producing sector",
72+
"Services-producing sector",
73+
]
74+
)
75+
)
76+
77+
# Calculate ranking based on monthly % change
78+
labour_processed = calculate_centered_rank(labour_filtered)
79+
80+
# Bin % difference
81+
labour_processed_cutted = cut_pdiff(labour_processed, DEFAULT_CUTS)
82+
labour_processed_filtered = labour_processed_cutted.filter(
83+
pl.col("YEAR") >= FILTER_YEAR[0], pl.col("YEAR") <= FILTER_YEAR[1]
84+
)
85+
86+
COLOR_MAPPING = {
87+
"(-inf, -0.05]": "#d82828ff",
88+
"(-0.05, -0.025]": "#fa6f1fff",
89+
"(-0.025, -0.012]": "#f1874aff",
90+
"(-0.012, -0.008]": "#f1b274ff",
91+
"(-0.008, -0.004]": "#FEE08B",
92+
"(-0.004, 0]": "#FFFFBF",
93+
"0": "#a8a8a8ff",
94+
"(0, 0.004]": "#E6F5D0",
95+
"(0.004, 0.008]": "#bce091ff",
96+
"(0.008, 0.012]": "#9ad65fff",
97+
"(0.012, 0.025]": "#78b552ff",
98+
"(0.025, 0.05]": "#5cb027ff",
99+
"(0.05, inf]": "#1f6fc6ff",
100+
}
101+
LEGEND_LABELS = [
102+
"-5%",
103+
"",
104+
"",
105+
"-1%",
106+
"",
107+
"",
108+
"No change",
109+
"",
110+
"",
111+
"1%",
112+
"",
113+
"",
114+
"5%",
115+
]
116+
```
117+
118+
Stats
119+
120+
```{python}
121+
def make_subtitle_for_industry(df, INDUSTRY):
122+
# Define offsets
123+
offsets = {
124+
"1M": 1,
125+
"5M": 5,
126+
"1Y": 12,
127+
"5Y": 60,
128+
}
129+
130+
# Sort by industry + date
131+
labour_offset = df
132+
labour_offset = labour_offset.sort(["Industry", "DATE_YMD"])
133+
134+
# Compute diffs and %diffs for each horizon
135+
for label, months in offsets.items():
136+
labour_offset = labour_offset.with_columns(
137+
[
138+
(pl.col("DATE_YMD").shift(months).alias(f"DATE_YMD_{label}")),
139+
(
140+
pl.col("VALUE")
141+
.shift(months)
142+
.over("Industry")
143+
.alias(f"VALUE_{label}")
144+
),
145+
(
146+
pl.col("VALUE") - pl.col("VALUE").shift(months).over("Industry")
147+
).alias(f"DIFF_{label}"),
148+
(
149+
(pl.col("VALUE") - pl.col("VALUE").shift(months).over("Industry"))
150+
/ pl.col("VALUE").shift(months).over("Industry")
151+
* 100
152+
).alias(f"PDIFF_{label}"),
153+
]
154+
)
155+
# convert to dictionary for easier access
156+
stats = labour_offset.filter(
157+
pl.col("Industry") == INDUSTRY, pl.col("DATE_YMD") == pl.col("DATE_YMD").max()
158+
).to_dicts()[0]
159+
160+
periods = [
161+
f"{stats['DIFF_1M'] * 1000:<+8,.0f} {f'({stats["PDIFF_1M"]:+.2f}%)':<10} Past Month",
162+
f"{stats['DIFF_5M'] * 1000:<+8,.0f} {f'({stats["PDIFF_5M"]:+.2f}%)':<10} Past 5 Months",
163+
f"{stats['DIFF_1Y'] * 1000:<+8,.0f} {f'({stats["PDIFF_1Y"]:+.2f}%)':<10} Past Year",
164+
f"{stats['DIFF_5Y'] * 1000:<+8,.0f} {f'({stats["PDIFF_5Y"]:+.2f}%)':<10} Past 5 Years",
165+
]
166+
167+
subtitle_text = "\n".join(periods)
168+
return subtitle_text
169+
```
170+
171+
# Horizontal legend with horizontal legend text
172+
173+
Initially I wanted a horizontal legend for the colors. But in order to remove the whitespace between keys, I discovered that the text needs to be smaller than the legend keys, otherwise they "push" the legend keys apart in uneven manner. I attempted to (*unsuccesfully*) address this by making the legend text small, eliminating as much text as possible (e.g. removing the "%" characters for `-0.50` and `0.50`), and lastly increasing the legend key size.
174+
175+
But it still didn't really work out the way I hoped, so I stuck with a vertical legend instead.
176+
177+
```{python}
178+
# | echo: false
179+
plot = (
180+
ggplot(
181+
labour_processed_cutted.filter(
182+
pl.col("YEAR") >= FILTER_YEAR[0], pl.col("YEAR") <= FILTER_YEAR[1]
183+
),
184+
aes(x="DATE_YMD", y="centered_rank_across_industry", fill="PDIFF_BINNED"),
185+
)
186+
+ geom_tile(color="white")
187+
+ theme_tufte()
188+
+ theme(
189+
figure_size=FIGURE_THEME_SIZE,
190+
axis_text_x=element_text(angle=90),
191+
legend_justification_right=1,
192+
legend_position="top",
193+
legend_text_position="bottom",
194+
legend_title_position="top",
195+
legend_key_spacing=0,
196+
legend_key_width=10,
197+
legend_key_height=10,
198+
legend_text=element_text(size=8),
199+
plot_background=element_rect(fill=COLOR_BACKGROUND, color=COLOR_BACKGROUND),
200+
)
201+
+ scale_fill_manual(values=COLOR_MAPPING, labels=LEGEND_LABELS)
202+
+ guides(fill=guide_legend(title="% Change From Previous Month", nrow=1))
203+
)
204+
plot
205+
```
206+
207+
# Composing in plotnine is not like R's patchwork
208+
209+
I wanted to add a line plot of employment numbers to the heatmap. Given the similar syntax in plotnine's [compose](https://plotnine.org/guide/plot-composition.html) to R's [patchwork](https://patchwork.data-imaginist.com/), I thought the behaviour would be similar.
210+
211+
One discrepancy is that there is no way to specify the relative size of component plots. But this might be addressed very soon [#980](https://github.com/has2k1/plotnine/pull/980)
212+
213+
It is possible to (rather labourously) pad plots by using `plot_spacer`s, which I attemp unsuccessfully below:
214+
215+
```{python}
216+
INDUSTRY = "Total employed, all industries"
217+
plot_data_subsetted = labour_processed_cutted.filter(pl.col("Industry") == INDUSTRY)
218+
219+
plot_highlight_industry = (
220+
plot
221+
+ geom_point(data=plot_data_subsetted, color="black", fill="black") # <3>
222+
+ labs(title=INDUSTRY, subtitle="")
223+
)
224+
plot_highlight_industry
225+
226+
line_plot = (
227+
ggplot(
228+
labour_processed_cutted.filter(
229+
pl.col("YEAR") >= FILTER_YEAR[0],
230+
pl.col("YEAR") <= FILTER_YEAR[1],
231+
pl.col("Industry").is_in([INDUSTRY]),
232+
),
233+
aes(x="DATE_YMD", y="VALUE"),
234+
)
235+
+ geom_line(color="black")
236+
+ theme_tufte()
237+
+ theme(
238+
legend_position="none",
239+
plot_title=element_text(size=10, ha="left"),
240+
axis_ticks_length=3,
241+
axis_ticks_major_y=element_line(),
242+
axis_text_y=element_text(size=8, margin={"r": 2, "l": 2, "units": "pt"}),
243+
plot_background=element_rect(fill=COLOR_BACKGROUND, color=COLOR_BACKGROUND),
244+
)
245+
+ scale_y_continuous(
246+
breaks=mb.breaks_extended(3),
247+
labels=lambda x: ["{:.0f}K".format(xi / 1000) for xi in x],
248+
)
249+
+ labs(title="Employment Rate")
250+
)
251+
252+
from plotnine.composition import Stack, plot_spacer
253+
254+
p1 = Stack(
255+
[
256+
line_plot + scale_x_datetime(expand=(0, 0)),
257+
plot_spacer(),
258+
plot_spacer(),
259+
plot_spacer(),
260+
]
261+
)
262+
263+
p2 = (
264+
plot_highlight_industry
265+
+ theme(plot_title=element_blank(), plot_subtitle=element_blank())
266+
+ scale_x_datetime(expand=(0, 0))
267+
)
268+
269+
Stack([p1, p2]) & scale_x_datetime(expand=(0, 0)) & theme_bw() & theme(
270+
plot_background=element_rect(fill=COLOR_BACKGROUND, color=COLOR_BACKGROUND)
271+
)
272+
```
273+
274+
The x axes don't automatically line up
275+
276+
This can be fixed by ensuring `expand` and the `limits` is the same:
277+
278+
```{python}
279+
Stack([plot_highlight_industry, line_plot]) & scale_x_datetime(
280+
expand=(0, 0)
281+
) & theme_bw() & theme(
282+
plot_background=element_rect(fill=COLOR_BACKGROUND, color=COLOR_BACKGROUND)
283+
)
284+
```
285+
286+
287+
But if we add `plot_spacer()`s then it won't line up because it seems that the space that the legend occupies is now ignored:
288+
289+
```{python}
290+
Stack([plot_highlight_industry, p1]) & scale_x_datetime(
291+
expand=(0, 0)
292+
) & theme_bw() & theme(
293+
plot_background=element_rect(fill=COLOR_BACKGROUND, color=COLOR_BACKGROUND)
294+
)
295+
```
296+
297+
Possibly there are some complexities that I don't fully understand [#959](https://github.com/has2k1/plotnine/issues/959), but at this point I decided to throw in the towel.

_quarto.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,6 @@ website:
2020
style: "floating"
2121
contents:
2222
- 02_develop_visualization.qmd
23+
- 03_things_that_didnt_work.qmd
2324
- 01_develop_data_processing.qmd
2425
alignment: center

0 commit comments

Comments
 (0)