workforce-plotting 0.2.0__tar.gz → 0.3.0__tar.gz
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.
- workforce_plotting-0.3.0/PKG-INFO +242 -0
- workforce_plotting-0.3.0/README.md +218 -0
- workforce_plotting-0.3.0/examples/default_examples.py +48 -0
- workforce_plotting-0.3.0/examples/paper_examples.py +130 -0
- {workforce_plotting-0.2.0 → workforce_plotting-0.3.0}/pyproject.toml +1 -1
- {workforce_plotting-0.2.0 → workforce_plotting-0.3.0}/src/workforce_plotting/__init__.py +2 -2
- workforce_plotting-0.3.0/src/workforce_plotting/figures.py +443 -0
- workforce_plotting-0.3.0/src/workforce_plotting.egg-info/PKG-INFO +242 -0
- {workforce_plotting-0.2.0 → workforce_plotting-0.3.0}/src/workforce_plotting.egg-info/SOURCES.txt +1 -0
- workforce_plotting-0.3.0/tests/test_plots.py +200 -0
- workforce_plotting-0.2.0/PKG-INFO +0 -135
- workforce_plotting-0.2.0/README.md +0 -111
- workforce_plotting-0.2.0/examples/paper_examples.py +0 -107
- workforce_plotting-0.2.0/src/workforce_plotting/figures.py +0 -158
- workforce_plotting-0.2.0/src/workforce_plotting.egg-info/PKG-INFO +0 -135
- workforce_plotting-0.2.0/tests/test_plots.py +0 -101
- {workforce_plotting-0.2.0 → workforce_plotting-0.3.0}/LICENSE +0 -0
- {workforce_plotting-0.2.0 → workforce_plotting-0.3.0}/MANIFEST.in +0 -0
- {workforce_plotting-0.2.0 → workforce_plotting-0.3.0}/setup.cfg +0 -0
- {workforce_plotting-0.2.0 → workforce_plotting-0.3.0}/src/workforce_plotting.egg-info/dependency_links.txt +0 -0
- {workforce_plotting-0.2.0 → workforce_plotting-0.3.0}/src/workforce_plotting.egg-info/requires.txt +0 -0
- {workforce_plotting-0.2.0 → workforce_plotting-0.3.0}/src/workforce_plotting.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: workforce-plotting
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Simple plotnine figures with Opportunity Insights styling.
|
|
5
|
+
Author: Corey Gelb-Bicknell
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/coreygb1/workforce-plotting
|
|
8
|
+
Requires-Python: >=3.12
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Dist: plotnine<0.16,>=0.15
|
|
12
|
+
Requires-Dist: pandas>=2.2
|
|
13
|
+
Requires-Dist: oi-tools>=0.11.7
|
|
14
|
+
Provides-Extra: polars
|
|
15
|
+
Requires-Dist: polars>=1; extra == "polars"
|
|
16
|
+
Requires-Dist: pyarrow>=14; extra == "polars"
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
19
|
+
Requires-Dist: build; extra == "dev"
|
|
20
|
+
Requires-Dist: twine; extra == "dev"
|
|
21
|
+
Requires-Dist: polars>=1; extra == "dev"
|
|
22
|
+
Requires-Dist: pyarrow>=14; extra == "dev"
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# workforce-plotting
|
|
26
|
+
|
|
27
|
+
Five small plotnine functions with a shared paper style based on Opportunity Insights. Supply
|
|
28
|
+
prepared estimates and confidence bounds; the functions only draw them. Every
|
|
29
|
+
function returns a `plotnine.ggplot` that you can edit with ordinary layers.
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
Python 3.12 or newer is required (matching `oi-tools`). For a Polars workflow:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
uv add 'workforce-plotting[polars]'
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Or use pip:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install 'workforce-plotting[polars]'
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
For local development from this checkout:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install -e '.[dev]'
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
For Polars input, install the `polars` extra, which also supplies the Arrow
|
|
52
|
+
conversion dependency. Pandas input needs no extra.
|
|
53
|
+
|
|
54
|
+
## Getting help
|
|
55
|
+
|
|
56
|
+
Function help includes every argument, its default, the expected column names,
|
|
57
|
+
and a short example:
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from workforce_plotting import plot_lines
|
|
61
|
+
help(plot_lines)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
In Jupyter, use `plot_lines?` or press Shift+Tab inside the parentheses. Column
|
|
65
|
+
arguments such as `x`, `y`, and `series` are names in your dataframe, not arrays.
|
|
66
|
+
`series="city"` means one color and legend entry per city; `plot_lines` also
|
|
67
|
+
connects observations separately within each city. Estimates and confidence
|
|
68
|
+
bounds must already be calculated. Every plot function returns an editable
|
|
69
|
+
plotnine plot, with no automatic saving.
|
|
70
|
+
|
|
71
|
+
## Functions
|
|
72
|
+
|
|
73
|
+
| Function | Use |
|
|
74
|
+
|---|---|
|
|
75
|
+
| `plot_lines(data, x=..., y=..., series=...)` | Event studies, trajectories, model complexity |
|
|
76
|
+
| `plot_validation(data, x=..., y=...)` | RCT versus digital-twin estimates with an identity line |
|
|
77
|
+
| `plot_bars(data, x=..., y=..., series=...)` | Already-calculated values by category |
|
|
78
|
+
| `plot_scatter(data, x=..., y=..., fitted=...)` | Unconnected points, optional CIs and a supplied fitted curve |
|
|
79
|
+
| `plot_intervals(data, x=..., y=..., lower=..., upper=...)` | Estimates and supplied confidence intervals |
|
|
80
|
+
| `theme_workforce()` | Times New Roman, gray axes, white background, no grid |
|
|
81
|
+
|
|
82
|
+
`plot_lines` accepts `lower` and `upper` columns and `points=False`.
|
|
83
|
+
`plot_scatter` also accepts `lower` and `upper` for supplied confidence bounds.
|
|
84
|
+
Its points remain unconnected even when a separate fitted curve is supplied.
|
|
85
|
+
`plot_validation` accepts `x_lower`, `x_upper`, `y_lower`, `y_upper`,
|
|
86
|
+
`gaps="horizontal"` or `"vertical"`, and a common `(min, max)` pair of `limits`.
|
|
87
|
+
The identity line has slope one in data units; the panel follows the figure's
|
|
88
|
+
aspect ratio. Other functions accept `series` for discrete groups. Use ordered
|
|
89
|
+
categorical columns to control color/legend order. Colors are teal, orange, navy,
|
|
90
|
+
purple, blue, red, and dark green from the public `oi_tools.figures.OIColors` API.
|
|
91
|
+
For a different mapping, add a plotnine manual color or fill scale.
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
import plotnine as pn
|
|
95
|
+
from workforce_plotting import plot_lines
|
|
96
|
+
|
|
97
|
+
p = plot_lines(df, x="year", y="earnings", series="group")
|
|
98
|
+
p += pn.labs(x="Years since enrollment", y="Mean annual earnings")
|
|
99
|
+
p.save("earnings.svg")
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Dataframes should be tidy: one row per plotted point, with separate columns for
|
|
103
|
+
x, y, and an optional series. Lines connect points in x order within each series.
|
|
104
|
+
Include a row with a missing y where a line should break; simply omitting a row
|
|
105
|
+
connects the remaining observations. Fitted lines use the same column names as
|
|
106
|
+
the scatter data. No smoothing, RMSE calculation, confidence-interval estimation,
|
|
107
|
+
or automatic saving happens inside the plotting functions.
|
|
108
|
+
|
|
109
|
+
Add `pn.coord_flip()` for horizontal bars/intervals; use ordinary plotnine scales
|
|
110
|
+
for dollars, percentages, limits, or log axes. The default font is Times New Roman; install it locally to match the paper.
|
|
111
|
+
Override fonts, dimensions, or text size with
|
|
112
|
+
`theme_workforce(base_family=..., base_size=..., width=..., height=...)`.
|
|
113
|
+
For example, `base_family="DejaVu Sans"` selects sans-serif type.
|
|
114
|
+
Titles, logos, notes, and paper-specific annotations belong in the example/caller.
|
|
115
|
+
|
|
116
|
+
## Paper defaults
|
|
117
|
+
|
|
118
|
+
- Times New Roman, 12-point text, 1.2 line spacing, and an 8-by-5-inch canvas.
|
|
119
|
+
- Gray 0.54-point axes/ticks, 10-point axis-title padding in both directions,
|
|
120
|
+
2% outer margins, five-point ticks, no grid, and an inside upper-left vertical
|
|
121
|
+
legend with a transparent background.
|
|
122
|
+
- Dots use plotnine size 3.75. Legend dots are 25% larger; connected-line plots
|
|
123
|
+
show dots in the legend when points are enabled and lines otherwise.
|
|
124
|
+
- Confidence intervals use a 0.54-point stroke. Dots and interval strokes are
|
|
125
|
+
separate layers, so changing one does not change the other.
|
|
126
|
+
|
|
127
|
+
Point-based functions accept `point_size`. Those with intervals also accept
|
|
128
|
+
`ci_width` (physical stroke width in points) and `cap_width` (data units,
|
|
129
|
+
perpendicular to the interval). A cap width of zero omits caps. `plot_lines`
|
|
130
|
+
defaults to 0.08; scatter, validation, and interval plots default to zero.
|
|
131
|
+
A data-unit cap width does not imply a fixed length in points across figures.
|
|
132
|
+
For grouped `plot_intervals`, cap widths are narrowed with the dodged groups.
|
|
133
|
+
|
|
134
|
+
Dollar formatting is optional, rather than imposed on every outcome:
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
from workforce_plotting import dollars
|
|
138
|
+
|
|
139
|
+
p += pn.scale_y_continuous(labels=dollars) # -$1K, $0, $2.5K, ...
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
`plot_validation` keeps its existing name and rectangular shape. Both axes
|
|
143
|
+
share numeric limits, but equal physical scaling is not imposed. Palette and
|
|
144
|
+
line-width defaults are unchanged.
|
|
145
|
+
|
|
146
|
+
Titles remain ordinary plotnine labels. Move the legend without changing the
|
|
147
|
+
plot constructor or shared theme:
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
p += pn.labs(title="Earnings Before and After Enrollment")
|
|
151
|
+
p += pn.theme(legend_position_inside=(0.97, 0.97),
|
|
152
|
+
legend_justification_inside=(1, 1)) # upper-right
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
To put it below the figure, use
|
|
156
|
+
`pn.theme(legend_position="bottom", legend_direction="horizontal")`.
|
|
157
|
+
|
|
158
|
+
## Event-study recipe
|
|
159
|
+
|
|
160
|
+
Use `plot_lines` with supplied estimates and confidence bounds, then add the
|
|
161
|
+
reference lines explicitly. Here treatment begins at event year zero; a zero-effect
|
|
162
|
+
line is added, but no treatment-timing line is imposed:
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
p = (
|
|
166
|
+
plot_lines(df, x="event_year", y="effect", lower="lower", upper="upper")
|
|
167
|
+
+ pn.geom_hline(yintercept=0, color="#A0A0A0", size=0.54)
|
|
168
|
+
+ pn.scale_y_continuous(labels=dollars)
|
|
169
|
+
+ pn.labs(x="Years Relative to Treatment", y="Earnings Effect")
|
|
170
|
+
)
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
For unconnected cohort estimates plus a supplied curve, use
|
|
174
|
+
`plot_scatter(df, x="year", y="effect", lower="lower", upper="upper", fitted=curve)`.
|
|
175
|
+
For estimates across categories without a curve, use `plot_intervals`.
|
|
176
|
+
|
|
177
|
+
## Styling your own plots
|
|
178
|
+
|
|
179
|
+
Use the theme and colors without using the plot constructors. `WFColors` is an
|
|
180
|
+
alias of `oi_tools.figures.OIColors`, and `theme_workforce` builds on `theme_oi`.
|
|
181
|
+
The `oi-tools` dependency remains; you only need workforce imports in your code.
|
|
182
|
+
All plotting functions and styling live together in `workforce_plotting/figures.py`.
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
import polars as pl
|
|
186
|
+
import plotnine as pn
|
|
187
|
+
from workforce_plotting import theme_workforce, WFColors
|
|
188
|
+
|
|
189
|
+
df = pl.DataFrame({"year": [2020, 2021, 2022], "earnings": [20000, 23000, 25000]})
|
|
190
|
+
p = (
|
|
191
|
+
pn.ggplot(df.to_pandas(), pn.aes(x="year", y="earnings"))
|
|
192
|
+
+ pn.geom_line(color=WFColors.TEAL)
|
|
193
|
+
+ pn.geom_point(color=WFColors.TEAL)
|
|
194
|
+
+ theme_workforce()
|
|
195
|
+
+ pn.labs(x="Year", y="Annual earnings")
|
|
196
|
+
)
|
|
197
|
+
p.save("earnings.svg")
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Plot constructors accept Polars directly. When building a plotnine plot yourself,
|
|
201
|
+
the explicit conversion above works with the supported plotnine version.
|
|
202
|
+
|
|
203
|
+
## Default outputs
|
|
204
|
+
|
|
205
|
+
`examples/default_examples.py` draws all five functions with their basic calls:
|
|
206
|
+
no added reference lines, titles, labels, scales, coordinates, or theme overrides.
|
|
207
|
+
It uses the same data for scatter and validation so their default behavior can
|
|
208
|
+
be compared directly. All values are invented.
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
python examples/default_examples.py outputs/defaults
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Titles remain available through `pn.labs(title=...)`; the plain default examples
|
|
215
|
+
deliberately do not add them.
|
|
216
|
+
|
|
217
|
+
## Editable examples
|
|
218
|
+
|
|
219
|
+
`examples/paper_examples.py` contains synthetic recipes for the paper/deck plot
|
|
220
|
+
families. These are invented values, not research results.
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
python examples/paper_examples.py outputs/examples
|
|
224
|
+
pytest
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Changes in 0.3
|
|
228
|
+
|
|
229
|
+
- Paper defaults: Times New Roman, inside vertical legends, five-point ticks,
|
|
230
|
+
shared margins, and consistent axis-title spacing. Choose a different font
|
|
231
|
+
explicitly with `theme_workforce(base_family=...)` for deck-style figures.
|
|
232
|
+
- Consistent dots, larger legend dots, and independently sized intervals.
|
|
233
|
+
- Optional scatter confidence intervals and an optional `dollars` labeler.
|
|
234
|
+
- Complete function help plus plain-default and customized example scripts.
|
|
235
|
+
|
|
236
|
+
The five plot names and the rectangular validation layout are unchanged.
|
|
237
|
+
|
|
238
|
+
## Changes from 0.1
|
|
239
|
+
|
|
240
|
+
This is a fresh API. The old Matplotlib functions, plotting dataclasses, curve
|
|
241
|
+
fitting machinery, and `aipw_analysis` compatibility package have been removed.
|
|
242
|
+
Plot creation and saving are now separate. The public API consists of the five plot functions, `theme_workforce`, `WFColors`, and the optional `dollars` formatter.
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# workforce-plotting
|
|
2
|
+
|
|
3
|
+
Five small plotnine functions with a shared paper style based on Opportunity Insights. Supply
|
|
4
|
+
prepared estimates and confidence bounds; the functions only draw them. Every
|
|
5
|
+
function returns a `plotnine.ggplot` that you can edit with ordinary layers.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
Python 3.12 or newer is required (matching `oi-tools`). For a Polars workflow:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
uv add 'workforce-plotting[polars]'
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Or use pip:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install 'workforce-plotting[polars]'
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
For local development from this checkout:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install -e '.[dev]'
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
For Polars input, install the `polars` extra, which also supplies the Arrow
|
|
28
|
+
conversion dependency. Pandas input needs no extra.
|
|
29
|
+
|
|
30
|
+
## Getting help
|
|
31
|
+
|
|
32
|
+
Function help includes every argument, its default, the expected column names,
|
|
33
|
+
and a short example:
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from workforce_plotting import plot_lines
|
|
37
|
+
help(plot_lines)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
In Jupyter, use `plot_lines?` or press Shift+Tab inside the parentheses. Column
|
|
41
|
+
arguments such as `x`, `y`, and `series` are names in your dataframe, not arrays.
|
|
42
|
+
`series="city"` means one color and legend entry per city; `plot_lines` also
|
|
43
|
+
connects observations separately within each city. Estimates and confidence
|
|
44
|
+
bounds must already be calculated. Every plot function returns an editable
|
|
45
|
+
plotnine plot, with no automatic saving.
|
|
46
|
+
|
|
47
|
+
## Functions
|
|
48
|
+
|
|
49
|
+
| Function | Use |
|
|
50
|
+
|---|---|
|
|
51
|
+
| `plot_lines(data, x=..., y=..., series=...)` | Event studies, trajectories, model complexity |
|
|
52
|
+
| `plot_validation(data, x=..., y=...)` | RCT versus digital-twin estimates with an identity line |
|
|
53
|
+
| `plot_bars(data, x=..., y=..., series=...)` | Already-calculated values by category |
|
|
54
|
+
| `plot_scatter(data, x=..., y=..., fitted=...)` | Unconnected points, optional CIs and a supplied fitted curve |
|
|
55
|
+
| `plot_intervals(data, x=..., y=..., lower=..., upper=...)` | Estimates and supplied confidence intervals |
|
|
56
|
+
| `theme_workforce()` | Times New Roman, gray axes, white background, no grid |
|
|
57
|
+
|
|
58
|
+
`plot_lines` accepts `lower` and `upper` columns and `points=False`.
|
|
59
|
+
`plot_scatter` also accepts `lower` and `upper` for supplied confidence bounds.
|
|
60
|
+
Its points remain unconnected even when a separate fitted curve is supplied.
|
|
61
|
+
`plot_validation` accepts `x_lower`, `x_upper`, `y_lower`, `y_upper`,
|
|
62
|
+
`gaps="horizontal"` or `"vertical"`, and a common `(min, max)` pair of `limits`.
|
|
63
|
+
The identity line has slope one in data units; the panel follows the figure's
|
|
64
|
+
aspect ratio. Other functions accept `series` for discrete groups. Use ordered
|
|
65
|
+
categorical columns to control color/legend order. Colors are teal, orange, navy,
|
|
66
|
+
purple, blue, red, and dark green from the public `oi_tools.figures.OIColors` API.
|
|
67
|
+
For a different mapping, add a plotnine manual color or fill scale.
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
import plotnine as pn
|
|
71
|
+
from workforce_plotting import plot_lines
|
|
72
|
+
|
|
73
|
+
p = plot_lines(df, x="year", y="earnings", series="group")
|
|
74
|
+
p += pn.labs(x="Years since enrollment", y="Mean annual earnings")
|
|
75
|
+
p.save("earnings.svg")
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Dataframes should be tidy: one row per plotted point, with separate columns for
|
|
79
|
+
x, y, and an optional series. Lines connect points in x order within each series.
|
|
80
|
+
Include a row with a missing y where a line should break; simply omitting a row
|
|
81
|
+
connects the remaining observations. Fitted lines use the same column names as
|
|
82
|
+
the scatter data. No smoothing, RMSE calculation, confidence-interval estimation,
|
|
83
|
+
or automatic saving happens inside the plotting functions.
|
|
84
|
+
|
|
85
|
+
Add `pn.coord_flip()` for horizontal bars/intervals; use ordinary plotnine scales
|
|
86
|
+
for dollars, percentages, limits, or log axes. The default font is Times New Roman; install it locally to match the paper.
|
|
87
|
+
Override fonts, dimensions, or text size with
|
|
88
|
+
`theme_workforce(base_family=..., base_size=..., width=..., height=...)`.
|
|
89
|
+
For example, `base_family="DejaVu Sans"` selects sans-serif type.
|
|
90
|
+
Titles, logos, notes, and paper-specific annotations belong in the example/caller.
|
|
91
|
+
|
|
92
|
+
## Paper defaults
|
|
93
|
+
|
|
94
|
+
- Times New Roman, 12-point text, 1.2 line spacing, and an 8-by-5-inch canvas.
|
|
95
|
+
- Gray 0.54-point axes/ticks, 10-point axis-title padding in both directions,
|
|
96
|
+
2% outer margins, five-point ticks, no grid, and an inside upper-left vertical
|
|
97
|
+
legend with a transparent background.
|
|
98
|
+
- Dots use plotnine size 3.75. Legend dots are 25% larger; connected-line plots
|
|
99
|
+
show dots in the legend when points are enabled and lines otherwise.
|
|
100
|
+
- Confidence intervals use a 0.54-point stroke. Dots and interval strokes are
|
|
101
|
+
separate layers, so changing one does not change the other.
|
|
102
|
+
|
|
103
|
+
Point-based functions accept `point_size`. Those with intervals also accept
|
|
104
|
+
`ci_width` (physical stroke width in points) and `cap_width` (data units,
|
|
105
|
+
perpendicular to the interval). A cap width of zero omits caps. `plot_lines`
|
|
106
|
+
defaults to 0.08; scatter, validation, and interval plots default to zero.
|
|
107
|
+
A data-unit cap width does not imply a fixed length in points across figures.
|
|
108
|
+
For grouped `plot_intervals`, cap widths are narrowed with the dodged groups.
|
|
109
|
+
|
|
110
|
+
Dollar formatting is optional, rather than imposed on every outcome:
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
from workforce_plotting import dollars
|
|
114
|
+
|
|
115
|
+
p += pn.scale_y_continuous(labels=dollars) # -$1K, $0, $2.5K, ...
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`plot_validation` keeps its existing name and rectangular shape. Both axes
|
|
119
|
+
share numeric limits, but equal physical scaling is not imposed. Palette and
|
|
120
|
+
line-width defaults are unchanged.
|
|
121
|
+
|
|
122
|
+
Titles remain ordinary plotnine labels. Move the legend without changing the
|
|
123
|
+
plot constructor or shared theme:
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
p += pn.labs(title="Earnings Before and After Enrollment")
|
|
127
|
+
p += pn.theme(legend_position_inside=(0.97, 0.97),
|
|
128
|
+
legend_justification_inside=(1, 1)) # upper-right
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
To put it below the figure, use
|
|
132
|
+
`pn.theme(legend_position="bottom", legend_direction="horizontal")`.
|
|
133
|
+
|
|
134
|
+
## Event-study recipe
|
|
135
|
+
|
|
136
|
+
Use `plot_lines` with supplied estimates and confidence bounds, then add the
|
|
137
|
+
reference lines explicitly. Here treatment begins at event year zero; a zero-effect
|
|
138
|
+
line is added, but no treatment-timing line is imposed:
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
p = (
|
|
142
|
+
plot_lines(df, x="event_year", y="effect", lower="lower", upper="upper")
|
|
143
|
+
+ pn.geom_hline(yintercept=0, color="#A0A0A0", size=0.54)
|
|
144
|
+
+ pn.scale_y_continuous(labels=dollars)
|
|
145
|
+
+ pn.labs(x="Years Relative to Treatment", y="Earnings Effect")
|
|
146
|
+
)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
For unconnected cohort estimates plus a supplied curve, use
|
|
150
|
+
`plot_scatter(df, x="year", y="effect", lower="lower", upper="upper", fitted=curve)`.
|
|
151
|
+
For estimates across categories without a curve, use `plot_intervals`.
|
|
152
|
+
|
|
153
|
+
## Styling your own plots
|
|
154
|
+
|
|
155
|
+
Use the theme and colors without using the plot constructors. `WFColors` is an
|
|
156
|
+
alias of `oi_tools.figures.OIColors`, and `theme_workforce` builds on `theme_oi`.
|
|
157
|
+
The `oi-tools` dependency remains; you only need workforce imports in your code.
|
|
158
|
+
All plotting functions and styling live together in `workforce_plotting/figures.py`.
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
import polars as pl
|
|
162
|
+
import plotnine as pn
|
|
163
|
+
from workforce_plotting import theme_workforce, WFColors
|
|
164
|
+
|
|
165
|
+
df = pl.DataFrame({"year": [2020, 2021, 2022], "earnings": [20000, 23000, 25000]})
|
|
166
|
+
p = (
|
|
167
|
+
pn.ggplot(df.to_pandas(), pn.aes(x="year", y="earnings"))
|
|
168
|
+
+ pn.geom_line(color=WFColors.TEAL)
|
|
169
|
+
+ pn.geom_point(color=WFColors.TEAL)
|
|
170
|
+
+ theme_workforce()
|
|
171
|
+
+ pn.labs(x="Year", y="Annual earnings")
|
|
172
|
+
)
|
|
173
|
+
p.save("earnings.svg")
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Plot constructors accept Polars directly. When building a plotnine plot yourself,
|
|
177
|
+
the explicit conversion above works with the supported plotnine version.
|
|
178
|
+
|
|
179
|
+
## Default outputs
|
|
180
|
+
|
|
181
|
+
`examples/default_examples.py` draws all five functions with their basic calls:
|
|
182
|
+
no added reference lines, titles, labels, scales, coordinates, or theme overrides.
|
|
183
|
+
It uses the same data for scatter and validation so their default behavior can
|
|
184
|
+
be compared directly. All values are invented.
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
python examples/default_examples.py outputs/defaults
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Titles remain available through `pn.labs(title=...)`; the plain default examples
|
|
191
|
+
deliberately do not add them.
|
|
192
|
+
|
|
193
|
+
## Editable examples
|
|
194
|
+
|
|
195
|
+
`examples/paper_examples.py` contains synthetic recipes for the paper/deck plot
|
|
196
|
+
families. These are invented values, not research results.
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
python examples/paper_examples.py outputs/examples
|
|
200
|
+
pytest
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Changes in 0.3
|
|
204
|
+
|
|
205
|
+
- Paper defaults: Times New Roman, inside vertical legends, five-point ticks,
|
|
206
|
+
shared margins, and consistent axis-title spacing. Choose a different font
|
|
207
|
+
explicitly with `theme_workforce(base_family=...)` for deck-style figures.
|
|
208
|
+
- Consistent dots, larger legend dots, and independently sized intervals.
|
|
209
|
+
- Optional scatter confidence intervals and an optional `dollars` labeler.
|
|
210
|
+
- Complete function help plus plain-default and customized example scripts.
|
|
211
|
+
|
|
212
|
+
The five plot names and the rectangular validation layout are unchanged.
|
|
213
|
+
|
|
214
|
+
## Changes from 0.1
|
|
215
|
+
|
|
216
|
+
This is a fresh API. The old Matplotlib functions, plotting dataclasses, curve
|
|
217
|
+
fitting machinery, and `aipw_analysis` compatibility package have been removed.
|
|
218
|
+
Plot creation and saving are now separate. The public API consists of the five plot functions, `theme_workforce`, `WFColors`, and the optional `dollars` formatter.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""The five plotting functions with no extra layers; all data are illustrative.
|
|
2
|
+
|
|
3
|
+
Run: python examples/default_examples.py outputs/defaults
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import polars as pl
|
|
10
|
+
|
|
11
|
+
from workforce_plotting import (
|
|
12
|
+
plot_bars, plot_intervals, plot_lines, plot_scatter, plot_validation,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def examples():
|
|
17
|
+
lines = pl.DataFrame({
|
|
18
|
+
"year": [2019, 2020, 2021, 2022, 2023] * 2,
|
|
19
|
+
"earnings": [12000, 14000, 18000, 22000, 25000,
|
|
20
|
+
12000, 13000, 15000, 18000, 20000],
|
|
21
|
+
"group": ["Group A"] * 5 + ["Group B"] * 5,
|
|
22
|
+
})
|
|
23
|
+
paired = pl.DataFrame({
|
|
24
|
+
"dml": [-1500, -1800, 900, 2500, 1800, 5200, 6500, 7200, 11000],
|
|
25
|
+
"rct": [-2500, -1000, 200, 1500, 2500, 4000, 6000, 8000, 10000],
|
|
26
|
+
})
|
|
27
|
+
bars = pl.DataFrame({"specification": ["A", "B", "C"], "rmse": [9500, 3800, 1800]})
|
|
28
|
+
intervals = pl.DataFrame({
|
|
29
|
+
"program": ["Program A", "Program B", "Program C"],
|
|
30
|
+
"effect": [1200, 2500, -500],
|
|
31
|
+
"lower": [200, 1400, -1500], "upper": [2200, 3600, 500],
|
|
32
|
+
})
|
|
33
|
+
return {
|
|
34
|
+
"plot_lines": plot_lines(lines, x="year", y="earnings", series="group"),
|
|
35
|
+
"plot_validation": plot_validation(paired, x="dml", y="rct"),
|
|
36
|
+
"plot_bars": plot_bars(bars, x="specification", y="rmse"),
|
|
37
|
+
"plot_scatter": plot_scatter(paired, x="dml", y="rct"),
|
|
38
|
+
"plot_intervals": plot_intervals(intervals, x="program", y="effect",
|
|
39
|
+
lower="lower", upper="upper"),
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
if __name__ == "__main__":
|
|
44
|
+
output = Path(sys.argv[1] if len(sys.argv) > 1 else "outputs/defaults")
|
|
45
|
+
output.mkdir(parents=True, exist_ok=True)
|
|
46
|
+
for name, plot in examples().items():
|
|
47
|
+
plot.save(output / f"{name}.png", dpi=180, verbose=False)
|
|
48
|
+
plot.save(output / f"{name}.svg", verbose=False)
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Paper-style recipes using invented values, not research results.
|
|
2
|
+
|
|
3
|
+
Run: python examples/paper_examples.py outputs/examples
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import polars as pl
|
|
10
|
+
import plotnine as pn
|
|
11
|
+
|
|
12
|
+
from workforce_plotting import (
|
|
13
|
+
dollars, plot_bars, plot_intervals, plot_lines, plot_scatter, plot_validation,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def examples():
|
|
18
|
+
# Connected event-study estimates. The analysis supplies the intervals.
|
|
19
|
+
event = pl.DataFrame({
|
|
20
|
+
"year": [-3, -2, -1, 0, 1, 2, 3] * 2,
|
|
21
|
+
"group": ["RCT"] * 7 + ["DML"] * 7,
|
|
22
|
+
"effect": [-100, 150, 0, 1200, 2100, 2500, 2700,
|
|
23
|
+
0, 100, 0, 1000, 2000, 2600, 2800],
|
|
24
|
+
}).with_columns(
|
|
25
|
+
pl.col("group").cast(pl.Enum(["RCT", "DML"])),
|
|
26
|
+
lower=pl.col("effect") - 400, upper=pl.col("effect") + 400,
|
|
27
|
+
)
|
|
28
|
+
event_study = (plot_lines(event, x="year", y="effect", series="group",
|
|
29
|
+
lower="lower", upper="upper", cap_width=0.05)
|
|
30
|
+
+ pn.geom_hline(yintercept=0, color="#A0A0A0", size=0.54)
|
|
31
|
+
+ pn.scale_y_continuous(labels=dollars)
|
|
32
|
+
+ pn.scale_x_continuous(breaks=list(range(-3, 4)))
|
|
33
|
+
+ pn.labs(title="Event study", x="Years Relative to Treatment", y="Earnings Effect"))
|
|
34
|
+
|
|
35
|
+
# Supplied RMSE estimates: neither fitting nor noise adjustment happens here.
|
|
36
|
+
complexity = pl.DataFrame({
|
|
37
|
+
"share": [1.0, 3.33, 10.0, 33.3, 100.0] * 4,
|
|
38
|
+
"sample": ["Full population"] * 5 + ["1%"] * 5
|
|
39
|
+
+ ["0.04% (CPS-sized)"] * 5 + ["0.02%"] * 5,
|
|
40
|
+
"rmse": [10000, 5800, 2300, 1600, 1500, 10200, 5800, 2500, 1800, 1800,
|
|
41
|
+
9800, 5800, 3300, 2600, 2600, 9200, 6500, 5100, 3900, 3400],
|
|
42
|
+
}).with_columns(pl.col("sample").cast(pl.Enum(
|
|
43
|
+
["Full population", "1%", "0.04% (CPS-sized)", "0.02%"])))
|
|
44
|
+
complexity_plot = (plot_lines(complexity, x="share", y="rmse", series="sample")
|
|
45
|
+
+ pn.scale_x_log10(breaks=[1, 3.33, 10, 33.3, 100],
|
|
46
|
+
labels=["1%", "3.33%", "10%", "33.3%", "100%"])
|
|
47
|
+
+ pn.scale_y_continuous(breaks=[0, 3000, 6000, 9000, 12000], labels=dollars)
|
|
48
|
+
+ pn.coord_cartesian(ylim=(0, 12000))
|
|
49
|
+
+ pn.labs(title="Model complexity", x="Model Complexity (% of Tuned Model)",
|
|
50
|
+
y="Noise-Adjusted RMSE")
|
|
51
|
+
+ pn.theme(legend_position_inside=(0.97, 0.97),
|
|
52
|
+
legend_justification_inside=(1, 1)))
|
|
53
|
+
|
|
54
|
+
validation = pl.DataFrame({
|
|
55
|
+
"rct": [-2500, -1000, 200, 1500, 2500, 4000, 6000, 8000, 10000],
|
|
56
|
+
"twin": [-1500, -1800, 900, 2500, 1800, 5200, 6500, 7200, 11000],
|
|
57
|
+
}).with_columns(
|
|
58
|
+
x_lower=pl.col("twin") - 900, x_upper=pl.col("twin") + 900,
|
|
59
|
+
y_lower=pl.col("rct") - 1300, y_upper=pl.col("rct") + 1300,
|
|
60
|
+
)
|
|
61
|
+
validation_plot = (plot_validation(validation, x="twin", y="rct",
|
|
62
|
+
x_lower="x_lower", x_upper="x_upper", y_lower="y_lower", y_upper="y_upper",
|
|
63
|
+
limits=(-5000, 12000), cap_width=150)
|
|
64
|
+
+ pn.scale_x_continuous(labels=dollars)
|
|
65
|
+
+ pn.scale_y_continuous(labels=dollars)
|
|
66
|
+
+ pn.labs(title="Validation", x="DML Treatment Effect", y="RCT Treatment Effect"))
|
|
67
|
+
|
|
68
|
+
comparisons = pl.DataFrame({
|
|
69
|
+
"specification": ["Demographics", "+ Earnings", "+ Geography"],
|
|
70
|
+
"rmse": [9500, 3800, 1800],
|
|
71
|
+
})
|
|
72
|
+
bars = (plot_bars(comparisons, x="specification", y="rmse")
|
|
73
|
+
+ pn.scale_x_discrete(limits=comparisons["specification"].to_list())
|
|
74
|
+
+ pn.scale_y_continuous(labels=dollars, expand=(0, 0, 0.1, 0))
|
|
75
|
+
+ pn.labs(title="Covariate comparison", x="", y="Noise-Adjusted RMSE"))
|
|
76
|
+
|
|
77
|
+
placement = pl.DataFrame({"placement": [20, 30, 40, 50, 60],
|
|
78
|
+
"effect": [1500, 2600, 2100, 4200, 4800]})
|
|
79
|
+
# The analysis supplies the fitted values; plot_scatter only draws them.
|
|
80
|
+
fitted = pl.DataFrame({"placement": [20, 60], "effect": [1300, 4700]})
|
|
81
|
+
scatter = (plot_scatter(placement, x="placement", y="effect", fitted=fitted)
|
|
82
|
+
+ pn.scale_y_continuous(labels=dollars)
|
|
83
|
+
+ pn.scale_x_continuous(labels=lambda xs: [f"{x:g}%" for x in xs])
|
|
84
|
+
+ pn.labs(title="Scatter with supplied fitted line", x="Placement Rate", y="Earnings Effect"))
|
|
85
|
+
|
|
86
|
+
intervals = pl.DataFrame({
|
|
87
|
+
"program": ["Program A", "Program B", "Program C"] * 2,
|
|
88
|
+
"estimator": ["RCT"] * 3 + ["DML"] * 3,
|
|
89
|
+
"effect": [1200, 2500, -500, 1500, 2200, -200],
|
|
90
|
+
"lower": [200, 1400, -1500, 900, 1600, -800],
|
|
91
|
+
"upper": [2200, 3600, 500, 2100, 2800, 400],
|
|
92
|
+
}).with_columns(pl.col("estimator").cast(pl.Enum(["RCT", "DML"])))
|
|
93
|
+
interval_plot = (plot_intervals(intervals, x="program", y="effect",
|
|
94
|
+
lower="lower", upper="upper", series="estimator")
|
|
95
|
+
+ pn.geom_hline(yintercept=0, color="#A0A0A0", size=0.54)
|
|
96
|
+
+ pn.coord_flip()
|
|
97
|
+
+ pn.scale_x_discrete(limits=["Program C", "Program B", "Program A"])
|
|
98
|
+
+ pn.scale_y_continuous(labels=dollars)
|
|
99
|
+
+ pn.labs(title="Estimates across programs", x="", y="Earnings Effect"))
|
|
100
|
+
|
|
101
|
+
cohorts = pl.DataFrame({
|
|
102
|
+
"year": [2010, 2012, 2014, 2016, 2018] * 2,
|
|
103
|
+
"city": ["City A"] * 5 + ["City B"] * 5,
|
|
104
|
+
"effect": [4000, 6000, 5500, 9000, 11000, 5000, 7500, 6500, 6000, 4000],
|
|
105
|
+
}).with_columns(pl.col("city").cast(pl.Enum(["City A", "City B"])),
|
|
106
|
+
lower=pl.col("effect") - 1500, upper=pl.col("effect") + 1500)
|
|
107
|
+
curves = pl.DataFrame({
|
|
108
|
+
"year": [2010, 2012, 2014, 2016, 2018] * 2,
|
|
109
|
+
"city": ["City A"] * 5 + ["City B"] * 5,
|
|
110
|
+
"effect": [4000, 5200, 6800, 8800, 11000, 5400, 6600, 6800, 6000, 4200],
|
|
111
|
+
}).with_columns(pl.col("city").cast(pl.Enum(["City A", "City B"])))
|
|
112
|
+
cohort_plot = (plot_scatter(cohorts, x="year", y="effect", series="city",
|
|
113
|
+
lower="lower", upper="upper", fitted=curves, cap_width=0.08)
|
|
114
|
+
+ pn.scale_y_continuous(labels=dollars)
|
|
115
|
+
+ pn.scale_x_continuous(breaks=[2010, 2012, 2014, 2016, 2018])
|
|
116
|
+
+ pn.labs(title="Cohorts with supplied curves", x="Enrollment Year", y="Earnings Effect"))
|
|
117
|
+
|
|
118
|
+
plots = {"event_study": event_study, "validation": validation_plot,
|
|
119
|
+
"bars": bars, "scatter": scatter, "intervals": interval_plot,
|
|
120
|
+
"cohorts": cohort_plot, "complexity": complexity_plot}
|
|
121
|
+
return {name: p + pn.labs(caption="Illustrative data; not research results.")
|
|
122
|
+
+ pn.theme(plot_caption=pn.element_text(size=9)) for name, p in plots.items()}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
if __name__ == "__main__":
|
|
126
|
+
output = Path(sys.argv[1] if len(sys.argv) > 1 else "outputs/examples")
|
|
127
|
+
output.mkdir(parents=True, exist_ok=True)
|
|
128
|
+
for name, plot in examples().items():
|
|
129
|
+
plot.save(output / f"{name}.png", dpi=180, verbose=False)
|
|
130
|
+
plot.save(output / f"{name}.svg", verbose=False)
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"""Plotnine figures and styling for workforce research."""
|
|
2
2
|
|
|
3
3
|
from .figures import (
|
|
4
|
-
WFColors, plot_bars, plot_intervals, plot_lines, plot_scatter,
|
|
4
|
+
WFColors, dollars, plot_bars, plot_intervals, plot_lines, plot_scatter,
|
|
5
5
|
plot_validation, theme_workforce,
|
|
6
6
|
)
|
|
7
7
|
|
|
8
8
|
__all__ = ["plot_lines", "plot_validation", "plot_bars", "plot_scatter",
|
|
9
|
-
"plot_intervals", "theme_workforce", "WFColors"]
|
|
9
|
+
"plot_intervals", "theme_workforce", "WFColors", "dollars"]
|