workforce-plotting 0.2.0__tar.gz → 0.4.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.4.0/PKG-INFO +263 -0
- workforce_plotting-0.4.0/README.md +239 -0
- workforce_plotting-0.4.0/examples/default_examples.py +48 -0
- workforce_plotting-0.4.0/examples/paper_examples.py +130 -0
- {workforce_plotting-0.2.0 → workforce_plotting-0.4.0}/pyproject.toml +1 -1
- {workforce_plotting-0.2.0 → workforce_plotting-0.4.0}/src/workforce_plotting/__init__.py +2 -2
- workforce_plotting-0.4.0/src/workforce_plotting/figures.py +394 -0
- workforce_plotting-0.4.0/src/workforce_plotting.egg-info/PKG-INFO +263 -0
- {workforce_plotting-0.2.0 → workforce_plotting-0.4.0}/src/workforce_plotting.egg-info/SOURCES.txt +1 -0
- workforce_plotting-0.4.0/tests/test_plots.py +355 -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.4.0}/LICENSE +0 -0
- {workforce_plotting-0.2.0 → workforce_plotting-0.4.0}/MANIFEST.in +0 -0
- {workforce_plotting-0.2.0 → workforce_plotting-0.4.0}/setup.cfg +0 -0
- {workforce_plotting-0.2.0 → workforce_plotting-0.4.0}/src/workforce_plotting.egg-info/dependency_links.txt +0 -0
- {workforce_plotting-0.2.0 → workforce_plotting-0.4.0}/src/workforce_plotting.egg-info/requires.txt +0 -0
- {workforce_plotting-0.2.0 → workforce_plotting-0.4.0}/src/workforce_plotting.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: workforce-plotting
|
|
3
|
+
Version: 0.4.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 describes the arguments and shows a short example.
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from workforce_plotting import plot_lines
|
|
60
|
+
help(plot_lines)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
In Jupyter, use `plot_lines?` or press Shift+Tab inside the parentheses. Column
|
|
64
|
+
arguments such as `x`, `y`, and `series` are names in your dataframe, not arrays.
|
|
65
|
+
`series="city"` means one color and legend entry per city; `plot_lines` also
|
|
66
|
+
connects observations separately within each city. Estimates and confidence
|
|
67
|
+
bounds must already be calculated. Every plot function returns an editable
|
|
68
|
+
plotnine plot, with no automatic saving.
|
|
69
|
+
|
|
70
|
+
## Functions
|
|
71
|
+
|
|
72
|
+
| Function | Use |
|
|
73
|
+
|---|---|
|
|
74
|
+
| `plot_lines(data, x=..., y=..., series=...)` | Event studies, trajectories, model complexity |
|
|
75
|
+
| `plot_validation(data, x=..., y=...)` | RCT versus digital-twin estimates with an identity line |
|
|
76
|
+
| `plot_bars(data, x=..., y=..., series=...)` | Already-calculated values by category |
|
|
77
|
+
| `plot_scatter(data, x=..., y=..., fitted=...)` | Unconnected points, optional CIs and a supplied fitted curve |
|
|
78
|
+
| `plot_intervals(data, x=..., y=..., lower=..., upper=...)` | Estimates and supplied confidence intervals |
|
|
79
|
+
| `theme_workforce()` | Times New Roman, gray axes, white background, no grid |
|
|
80
|
+
|
|
81
|
+
`plot_lines` accepts `lower` and `upper` columns and `points=False`.
|
|
82
|
+
`plot_scatter` also accepts `lower` and `upper` for supplied confidence bounds.
|
|
83
|
+
Its points remain unconnected even when a separate fitted curve is supplied.
|
|
84
|
+
`plot_validation` accepts `x_lower`, `x_upper`, `y_lower`, `y_upper`,
|
|
85
|
+
`gaps="horizontal"` or `"vertical"`, and a common `(min, max)` pair of `limits`.
|
|
86
|
+
Use `plot_validation(df, x="dml", y="rct", series="program_name")`
|
|
87
|
+
to color the estimates, intervals, and gap lines by program.
|
|
88
|
+
The identity line has slope one in data units; the panel follows the figure's
|
|
89
|
+
aspect ratio. All five functions accept `series` for discrete groups. Use ordered
|
|
90
|
+
categorical columns to control color/legend order. Colors are teal, orange, navy,
|
|
91
|
+
purple, blue, red, and dark green from the public `oi_tools.figures.OIColors` API.
|
|
92
|
+
For a different mapping, add a plotnine manual color or fill scale.
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
import plotnine as pn
|
|
96
|
+
from workforce_plotting import plot_lines
|
|
97
|
+
|
|
98
|
+
p = plot_lines(df, x="year", y="earnings", series="group")
|
|
99
|
+
p += pn.labs(x="Years since enrollment", y="Mean annual earnings")
|
|
100
|
+
p.save("earnings.svg")
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Dataframes should be tidy: one row per plotted point, with separate columns for
|
|
104
|
+
x, y, and an optional series. Lines connect points in x order within each series.
|
|
105
|
+
Include a row with a missing y where a line should break; simply omitting a row
|
|
106
|
+
connects the remaining observations. Fitted lines use the same column names as
|
|
107
|
+
the scatter data. No smoothing, RMSE calculation, confidence-interval estimation,
|
|
108
|
+
or automatic saving happens inside the plotting functions.
|
|
109
|
+
|
|
110
|
+
Add `pn.coord_flip()` for horizontal bars/intervals; use ordinary plotnine scales
|
|
111
|
+
for dollars, percentages, limits, or log axes. The default font is Times New Roman when installed. The theme checks for
|
|
112
|
+
DejaVu Serif, DejaVu Sans, STIXGeneral, and other available families before
|
|
113
|
+
rendering. An empty font list triggers registration of the bundled DejaVu Serif.
|
|
114
|
+
Override fonts, dimensions, or text size with
|
|
115
|
+
`theme_workforce(base_family=..., base_size=..., width=..., height=...)`.
|
|
116
|
+
For example, `base_family="DejaVu Sans"` selects sans-serif type.
|
|
117
|
+
Use `title=` for a plot title. Add logos, notes, and paper-specific annotations
|
|
118
|
+
with plotnine.
|
|
119
|
+
|
|
120
|
+
## Paper defaults
|
|
121
|
+
|
|
122
|
+
- Times New Roman when available, 12-point text, 1.2 line spacing, and an
|
|
123
|
+
8-by-5-inch canvas.
|
|
124
|
+
- Gray 0.54-point axes/ticks, 10-point axis-title padding in both directions,
|
|
125
|
+
2% outer margins, 4.14-point ticks, no grid, and an inside upper-left vertical
|
|
126
|
+
legend with a transparent background.
|
|
127
|
+
- Dots use plotnine size 3.75. Legend dots are 25% larger; connected-line plots
|
|
128
|
+
show dots in the legend when points are enabled and lines otherwise.
|
|
129
|
+
- Confidence intervals use a 0.54-point stroke. Dots and interval strokes are
|
|
130
|
+
separate layers, so changing one does not change the other.
|
|
131
|
+
|
|
132
|
+
All five functions accept `title=` for a centered plot title. Grouped bars,
|
|
133
|
+
validation plots, and intervals on categorical axes put the legend above the panel.
|
|
134
|
+
For longer category names, supply
|
|
135
|
+
labels with line breaks or use `scale_x_discrete(labels=...)` to wrap them.
|
|
136
|
+
Datetime x-axes covering less than a day show clock times in the data's time zone.
|
|
137
|
+
|
|
138
|
+
Point-based functions accept `point_size`. Those with intervals also accept
|
|
139
|
+
`ci_width` (stroke width in points) and `cap_width` (plot-axis units
|
|
140
|
+
perpendicular to the interval). Caps default to 1% of the numeric axis span
|
|
141
|
+
or 0.12 category units on a categorical axis. A single numeric x position
|
|
142
|
+
has no default caps. On a log axis, the cap width
|
|
143
|
+
uses the transformed axis units. Set `cap_width=0` to omit caps. Grouped
|
|
144
|
+
`plot_intervals` narrows the default caps when x positions are closely spaced.
|
|
145
|
+
|
|
146
|
+
Dollar formatting is optional, rather than imposed on every outcome:
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
from workforce_plotting import dollars
|
|
150
|
+
|
|
151
|
+
p += pn.scale_y_continuous(labels=dollars) # -$1K, $0, $2.5K, ...
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
`plot_validation` keeps its existing name and rectangular shape. Both axes
|
|
155
|
+
share numeric limits, but equal physical scaling is not imposed.
|
|
156
|
+
|
|
157
|
+
Titles remain ordinary plotnine labels. Move the legend without changing the
|
|
158
|
+
plot constructor or shared theme:
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
p += pn.labs(title="Earnings Before and After Enrollment")
|
|
162
|
+
p += pn.theme(legend_position="inside", legend_position_inside=(0.97, 0.97),
|
|
163
|
+
legend_justification_inside=(1, 1)) # upper-right
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
To put it below the figure, use
|
|
167
|
+
`pn.theme(legend_position="bottom", legend_direction="horizontal")`.
|
|
168
|
+
|
|
169
|
+
## Event-study recipe
|
|
170
|
+
|
|
171
|
+
Use `plot_lines` with supplied estimates and confidence bounds, then add the
|
|
172
|
+
reference lines explicitly. Here treatment begins at event year zero; a zero-effect
|
|
173
|
+
line is added, but no treatment-timing line is imposed:
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
p = (
|
|
177
|
+
plot_lines(df, x="event_year", y="effect", lower="lower", upper="upper")
|
|
178
|
+
+ pn.geom_hline(yintercept=0, color="#A0A0A0", size=0.54)
|
|
179
|
+
+ pn.scale_y_continuous(labels=dollars)
|
|
180
|
+
+ pn.labs(x="Years Relative to Treatment", y="Earnings Effect")
|
|
181
|
+
)
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
For unconnected cohort estimates plus a supplied curve, use
|
|
185
|
+
`plot_scatter(df, x="year", y="effect", lower="lower", upper="upper", fitted=curve)`.
|
|
186
|
+
For estimates across categories, use `plot_intervals`. Pass `fitted=` to draw a
|
|
187
|
+
curve from supplied values. For a single series, a supplied curve appears in teal
|
|
188
|
+
with gray points in both `plot_intervals` and `plot_scatter`.
|
|
189
|
+
|
|
190
|
+
## Styling your own plots
|
|
191
|
+
|
|
192
|
+
Use the theme and colors without using the plot constructors. `WFColors` is an
|
|
193
|
+
alias of `oi_tools.figures.OIColors`, and `theme_workforce` builds on `theme_oi`.
|
|
194
|
+
The `oi-tools` dependency remains; you only need workforce imports in your code.
|
|
195
|
+
All plotting functions and styling live together in `workforce_plotting/figures.py`.
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
import polars as pl
|
|
199
|
+
import plotnine as pn
|
|
200
|
+
from workforce_plotting import theme_workforce, WFColors
|
|
201
|
+
|
|
202
|
+
df = pl.DataFrame({"year": [2020, 2021, 2022], "earnings": [20000, 23000, 25000]})
|
|
203
|
+
p = (
|
|
204
|
+
pn.ggplot(df.to_pandas(), pn.aes(x="year", y="earnings"))
|
|
205
|
+
+ pn.geom_line(color=WFColors.TEAL)
|
|
206
|
+
+ pn.geom_point(color=WFColors.TEAL)
|
|
207
|
+
+ theme_workforce()
|
|
208
|
+
+ pn.labs(x="Year", y="Annual earnings")
|
|
209
|
+
)
|
|
210
|
+
p.save("earnings.svg")
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Plot constructors accept Polars directly. When building a plotnine plot yourself,
|
|
214
|
+
the explicit conversion above works with the supported plotnine version.
|
|
215
|
+
|
|
216
|
+
## Default outputs
|
|
217
|
+
|
|
218
|
+
`examples/default_examples.py` draws all five functions with their basic calls:
|
|
219
|
+
no added reference lines, titles, labels, scales, coordinates, or theme overrides.
|
|
220
|
+
It uses the same data for scatter and validation so their default behavior can
|
|
221
|
+
be compared directly. All values are invented.
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
python examples/default_examples.py outputs/defaults
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Titles remain available through `pn.labs(title=...)`; the plain default examples
|
|
228
|
+
deliberately do not add them.
|
|
229
|
+
|
|
230
|
+
## Editable examples
|
|
231
|
+
|
|
232
|
+
`examples/paper_examples.py` contains synthetic recipes for the paper/deck plot
|
|
233
|
+
families. These are invented values, not research results.
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
python examples/paper_examples.py outputs/examples
|
|
237
|
+
pytest
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
## Changes in 0.4
|
|
241
|
+
|
|
242
|
+
- Validation plots accept `series=` to color by program or another grouping column.
|
|
243
|
+
- Plot titles are larger and centered, confidence intervals have short caps by
|
|
244
|
+
default, and interval plots accept a supplied fit.
|
|
245
|
+
- Numeric group labels, sparse groups, missing estimates, and intraday axes
|
|
246
|
+
render more reliably.
|
|
247
|
+
|
|
248
|
+
## Changes in 0.3
|
|
249
|
+
|
|
250
|
+
- Paper defaults: Times New Roman, inside vertical legends, five-point ticks,
|
|
251
|
+
shared margins, and consistent axis-title spacing. Choose a different font
|
|
252
|
+
explicitly with `theme_workforce(base_family=...)` for deck-style figures.
|
|
253
|
+
- Consistent dots, larger legend dots, and independently sized intervals.
|
|
254
|
+
- Optional scatter confidence intervals and an optional `dollars` labeler.
|
|
255
|
+
- Complete function help plus plain-default and customized example scripts.
|
|
256
|
+
|
|
257
|
+
The five plot names and the rectangular validation layout are unchanged.
|
|
258
|
+
|
|
259
|
+
## Changes from 0.1
|
|
260
|
+
|
|
261
|
+
This is a fresh API. The old Matplotlib functions, plotting dataclasses, curve
|
|
262
|
+
fitting machinery, and `aipw_analysis` compatibility package have been removed.
|
|
263
|
+
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,239 @@
|
|
|
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 describes the arguments and shows a short example.
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from workforce_plotting import plot_lines
|
|
36
|
+
help(plot_lines)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
In Jupyter, use `plot_lines?` or press Shift+Tab inside the parentheses. Column
|
|
40
|
+
arguments such as `x`, `y`, and `series` are names in your dataframe, not arrays.
|
|
41
|
+
`series="city"` means one color and legend entry per city; `plot_lines` also
|
|
42
|
+
connects observations separately within each city. Estimates and confidence
|
|
43
|
+
bounds must already be calculated. Every plot function returns an editable
|
|
44
|
+
plotnine plot, with no automatic saving.
|
|
45
|
+
|
|
46
|
+
## Functions
|
|
47
|
+
|
|
48
|
+
| Function | Use |
|
|
49
|
+
|---|---|
|
|
50
|
+
| `plot_lines(data, x=..., y=..., series=...)` | Event studies, trajectories, model complexity |
|
|
51
|
+
| `plot_validation(data, x=..., y=...)` | RCT versus digital-twin estimates with an identity line |
|
|
52
|
+
| `plot_bars(data, x=..., y=..., series=...)` | Already-calculated values by category |
|
|
53
|
+
| `plot_scatter(data, x=..., y=..., fitted=...)` | Unconnected points, optional CIs and a supplied fitted curve |
|
|
54
|
+
| `plot_intervals(data, x=..., y=..., lower=..., upper=...)` | Estimates and supplied confidence intervals |
|
|
55
|
+
| `theme_workforce()` | Times New Roman, gray axes, white background, no grid |
|
|
56
|
+
|
|
57
|
+
`plot_lines` accepts `lower` and `upper` columns and `points=False`.
|
|
58
|
+
`plot_scatter` also accepts `lower` and `upper` for supplied confidence bounds.
|
|
59
|
+
Its points remain unconnected even when a separate fitted curve is supplied.
|
|
60
|
+
`plot_validation` accepts `x_lower`, `x_upper`, `y_lower`, `y_upper`,
|
|
61
|
+
`gaps="horizontal"` or `"vertical"`, and a common `(min, max)` pair of `limits`.
|
|
62
|
+
Use `plot_validation(df, x="dml", y="rct", series="program_name")`
|
|
63
|
+
to color the estimates, intervals, and gap lines by program.
|
|
64
|
+
The identity line has slope one in data units; the panel follows the figure's
|
|
65
|
+
aspect ratio. All five functions accept `series` for discrete groups. Use ordered
|
|
66
|
+
categorical columns to control color/legend order. Colors are teal, orange, navy,
|
|
67
|
+
purple, blue, red, and dark green from the public `oi_tools.figures.OIColors` API.
|
|
68
|
+
For a different mapping, add a plotnine manual color or fill scale.
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
import plotnine as pn
|
|
72
|
+
from workforce_plotting import plot_lines
|
|
73
|
+
|
|
74
|
+
p = plot_lines(df, x="year", y="earnings", series="group")
|
|
75
|
+
p += pn.labs(x="Years since enrollment", y="Mean annual earnings")
|
|
76
|
+
p.save("earnings.svg")
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Dataframes should be tidy: one row per plotted point, with separate columns for
|
|
80
|
+
x, y, and an optional series. Lines connect points in x order within each series.
|
|
81
|
+
Include a row with a missing y where a line should break; simply omitting a row
|
|
82
|
+
connects the remaining observations. Fitted lines use the same column names as
|
|
83
|
+
the scatter data. No smoothing, RMSE calculation, confidence-interval estimation,
|
|
84
|
+
or automatic saving happens inside the plotting functions.
|
|
85
|
+
|
|
86
|
+
Add `pn.coord_flip()` for horizontal bars/intervals; use ordinary plotnine scales
|
|
87
|
+
for dollars, percentages, limits, or log axes. The default font is Times New Roman when installed. The theme checks for
|
|
88
|
+
DejaVu Serif, DejaVu Sans, STIXGeneral, and other available families before
|
|
89
|
+
rendering. An empty font list triggers registration of the bundled DejaVu Serif.
|
|
90
|
+
Override fonts, dimensions, or text size with
|
|
91
|
+
`theme_workforce(base_family=..., base_size=..., width=..., height=...)`.
|
|
92
|
+
For example, `base_family="DejaVu Sans"` selects sans-serif type.
|
|
93
|
+
Use `title=` for a plot title. Add logos, notes, and paper-specific annotations
|
|
94
|
+
with plotnine.
|
|
95
|
+
|
|
96
|
+
## Paper defaults
|
|
97
|
+
|
|
98
|
+
- Times New Roman when available, 12-point text, 1.2 line spacing, and an
|
|
99
|
+
8-by-5-inch canvas.
|
|
100
|
+
- Gray 0.54-point axes/ticks, 10-point axis-title padding in both directions,
|
|
101
|
+
2% outer margins, 4.14-point ticks, no grid, and an inside upper-left vertical
|
|
102
|
+
legend with a transparent background.
|
|
103
|
+
- Dots use plotnine size 3.75. Legend dots are 25% larger; connected-line plots
|
|
104
|
+
show dots in the legend when points are enabled and lines otherwise.
|
|
105
|
+
- Confidence intervals use a 0.54-point stroke. Dots and interval strokes are
|
|
106
|
+
separate layers, so changing one does not change the other.
|
|
107
|
+
|
|
108
|
+
All five functions accept `title=` for a centered plot title. Grouped bars,
|
|
109
|
+
validation plots, and intervals on categorical axes put the legend above the panel.
|
|
110
|
+
For longer category names, supply
|
|
111
|
+
labels with line breaks or use `scale_x_discrete(labels=...)` to wrap them.
|
|
112
|
+
Datetime x-axes covering less than a day show clock times in the data's time zone.
|
|
113
|
+
|
|
114
|
+
Point-based functions accept `point_size`. Those with intervals also accept
|
|
115
|
+
`ci_width` (stroke width in points) and `cap_width` (plot-axis units
|
|
116
|
+
perpendicular to the interval). Caps default to 1% of the numeric axis span
|
|
117
|
+
or 0.12 category units on a categorical axis. A single numeric x position
|
|
118
|
+
has no default caps. On a log axis, the cap width
|
|
119
|
+
uses the transformed axis units. Set `cap_width=0` to omit caps. Grouped
|
|
120
|
+
`plot_intervals` narrows the default caps when x positions are closely spaced.
|
|
121
|
+
|
|
122
|
+
Dollar formatting is optional, rather than imposed on every outcome:
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
from workforce_plotting import dollars
|
|
126
|
+
|
|
127
|
+
p += pn.scale_y_continuous(labels=dollars) # -$1K, $0, $2.5K, ...
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
`plot_validation` keeps its existing name and rectangular shape. Both axes
|
|
131
|
+
share numeric limits, but equal physical scaling is not imposed.
|
|
132
|
+
|
|
133
|
+
Titles remain ordinary plotnine labels. Move the legend without changing the
|
|
134
|
+
plot constructor or shared theme:
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
p += pn.labs(title="Earnings Before and After Enrollment")
|
|
138
|
+
p += pn.theme(legend_position="inside", legend_position_inside=(0.97, 0.97),
|
|
139
|
+
legend_justification_inside=(1, 1)) # upper-right
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
To put it below the figure, use
|
|
143
|
+
`pn.theme(legend_position="bottom", legend_direction="horizontal")`.
|
|
144
|
+
|
|
145
|
+
## Event-study recipe
|
|
146
|
+
|
|
147
|
+
Use `plot_lines` with supplied estimates and confidence bounds, then add the
|
|
148
|
+
reference lines explicitly. Here treatment begins at event year zero; a zero-effect
|
|
149
|
+
line is added, but no treatment-timing line is imposed:
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
p = (
|
|
153
|
+
plot_lines(df, x="event_year", y="effect", lower="lower", upper="upper")
|
|
154
|
+
+ pn.geom_hline(yintercept=0, color="#A0A0A0", size=0.54)
|
|
155
|
+
+ pn.scale_y_continuous(labels=dollars)
|
|
156
|
+
+ pn.labs(x="Years Relative to Treatment", y="Earnings Effect")
|
|
157
|
+
)
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
For unconnected cohort estimates plus a supplied curve, use
|
|
161
|
+
`plot_scatter(df, x="year", y="effect", lower="lower", upper="upper", fitted=curve)`.
|
|
162
|
+
For estimates across categories, use `plot_intervals`. Pass `fitted=` to draw a
|
|
163
|
+
curve from supplied values. For a single series, a supplied curve appears in teal
|
|
164
|
+
with gray points in both `plot_intervals` and `plot_scatter`.
|
|
165
|
+
|
|
166
|
+
## Styling your own plots
|
|
167
|
+
|
|
168
|
+
Use the theme and colors without using the plot constructors. `WFColors` is an
|
|
169
|
+
alias of `oi_tools.figures.OIColors`, and `theme_workforce` builds on `theme_oi`.
|
|
170
|
+
The `oi-tools` dependency remains; you only need workforce imports in your code.
|
|
171
|
+
All plotting functions and styling live together in `workforce_plotting/figures.py`.
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
import polars as pl
|
|
175
|
+
import plotnine as pn
|
|
176
|
+
from workforce_plotting import theme_workforce, WFColors
|
|
177
|
+
|
|
178
|
+
df = pl.DataFrame({"year": [2020, 2021, 2022], "earnings": [20000, 23000, 25000]})
|
|
179
|
+
p = (
|
|
180
|
+
pn.ggplot(df.to_pandas(), pn.aes(x="year", y="earnings"))
|
|
181
|
+
+ pn.geom_line(color=WFColors.TEAL)
|
|
182
|
+
+ pn.geom_point(color=WFColors.TEAL)
|
|
183
|
+
+ theme_workforce()
|
|
184
|
+
+ pn.labs(x="Year", y="Annual earnings")
|
|
185
|
+
)
|
|
186
|
+
p.save("earnings.svg")
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Plot constructors accept Polars directly. When building a plotnine plot yourself,
|
|
190
|
+
the explicit conversion above works with the supported plotnine version.
|
|
191
|
+
|
|
192
|
+
## Default outputs
|
|
193
|
+
|
|
194
|
+
`examples/default_examples.py` draws all five functions with their basic calls:
|
|
195
|
+
no added reference lines, titles, labels, scales, coordinates, or theme overrides.
|
|
196
|
+
It uses the same data for scatter and validation so their default behavior can
|
|
197
|
+
be compared directly. All values are invented.
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
python examples/default_examples.py outputs/defaults
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Titles remain available through `pn.labs(title=...)`; the plain default examples
|
|
204
|
+
deliberately do not add them.
|
|
205
|
+
|
|
206
|
+
## Editable examples
|
|
207
|
+
|
|
208
|
+
`examples/paper_examples.py` contains synthetic recipes for the paper/deck plot
|
|
209
|
+
families. These are invented values, not research results.
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
python examples/paper_examples.py outputs/examples
|
|
213
|
+
pytest
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## Changes in 0.4
|
|
217
|
+
|
|
218
|
+
- Validation plots accept `series=` to color by program or another grouping column.
|
|
219
|
+
- Plot titles are larger and centered, confidence intervals have short caps by
|
|
220
|
+
default, and interval plots accept a supplied fit.
|
|
221
|
+
- Numeric group labels, sparse groups, missing estimates, and intraday axes
|
|
222
|
+
render more reliably.
|
|
223
|
+
|
|
224
|
+
## Changes in 0.3
|
|
225
|
+
|
|
226
|
+
- Paper defaults: Times New Roman, inside vertical legends, five-point ticks,
|
|
227
|
+
shared margins, and consistent axis-title spacing. Choose a different font
|
|
228
|
+
explicitly with `theme_workforce(base_family=...)` for deck-style figures.
|
|
229
|
+
- Consistent dots, larger legend dots, and independently sized intervals.
|
|
230
|
+
- Optional scatter confidence intervals and an optional `dollars` labeler.
|
|
231
|
+
- Complete function help plus plain-default and customized example scripts.
|
|
232
|
+
|
|
233
|
+
The five plot names and the rectangular validation layout are unchanged.
|
|
234
|
+
|
|
235
|
+
## Changes from 0.1
|
|
236
|
+
|
|
237
|
+
This is a fresh API. The old Matplotlib functions, plotting dataclasses, curve
|
|
238
|
+
fitting machinery, and `aipw_analysis` compatibility package have been removed.
|
|
239
|
+
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)
|