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.
Files changed (22) hide show
  1. workforce_plotting-0.4.0/PKG-INFO +263 -0
  2. workforce_plotting-0.4.0/README.md +239 -0
  3. workforce_plotting-0.4.0/examples/default_examples.py +48 -0
  4. workforce_plotting-0.4.0/examples/paper_examples.py +130 -0
  5. {workforce_plotting-0.2.0 → workforce_plotting-0.4.0}/pyproject.toml +1 -1
  6. {workforce_plotting-0.2.0 → workforce_plotting-0.4.0}/src/workforce_plotting/__init__.py +2 -2
  7. workforce_plotting-0.4.0/src/workforce_plotting/figures.py +394 -0
  8. workforce_plotting-0.4.0/src/workforce_plotting.egg-info/PKG-INFO +263 -0
  9. {workforce_plotting-0.2.0 → workforce_plotting-0.4.0}/src/workforce_plotting.egg-info/SOURCES.txt +1 -0
  10. workforce_plotting-0.4.0/tests/test_plots.py +355 -0
  11. workforce_plotting-0.2.0/PKG-INFO +0 -135
  12. workforce_plotting-0.2.0/README.md +0 -111
  13. workforce_plotting-0.2.0/examples/paper_examples.py +0 -107
  14. workforce_plotting-0.2.0/src/workforce_plotting/figures.py +0 -158
  15. workforce_plotting-0.2.0/src/workforce_plotting.egg-info/PKG-INFO +0 -135
  16. workforce_plotting-0.2.0/tests/test_plots.py +0 -101
  17. {workforce_plotting-0.2.0 → workforce_plotting-0.4.0}/LICENSE +0 -0
  18. {workforce_plotting-0.2.0 → workforce_plotting-0.4.0}/MANIFEST.in +0 -0
  19. {workforce_plotting-0.2.0 → workforce_plotting-0.4.0}/setup.cfg +0 -0
  20. {workforce_plotting-0.2.0 → workforce_plotting-0.4.0}/src/workforce_plotting.egg-info/dependency_links.txt +0 -0
  21. {workforce_plotting-0.2.0 → workforce_plotting-0.4.0}/src/workforce_plotting.egg-info/requires.txt +0 -0
  22. {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)