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.
Files changed (22) hide show
  1. workforce_plotting-0.3.0/PKG-INFO +242 -0
  2. workforce_plotting-0.3.0/README.md +218 -0
  3. workforce_plotting-0.3.0/examples/default_examples.py +48 -0
  4. workforce_plotting-0.3.0/examples/paper_examples.py +130 -0
  5. {workforce_plotting-0.2.0 → workforce_plotting-0.3.0}/pyproject.toml +1 -1
  6. {workforce_plotting-0.2.0 → workforce_plotting-0.3.0}/src/workforce_plotting/__init__.py +2 -2
  7. workforce_plotting-0.3.0/src/workforce_plotting/figures.py +443 -0
  8. workforce_plotting-0.3.0/src/workforce_plotting.egg-info/PKG-INFO +242 -0
  9. {workforce_plotting-0.2.0 → workforce_plotting-0.3.0}/src/workforce_plotting.egg-info/SOURCES.txt +1 -0
  10. workforce_plotting-0.3.0/tests/test_plots.py +200 -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.3.0}/LICENSE +0 -0
  18. {workforce_plotting-0.2.0 → workforce_plotting-0.3.0}/MANIFEST.in +0 -0
  19. {workforce_plotting-0.2.0 → workforce_plotting-0.3.0}/setup.cfg +0 -0
  20. {workforce_plotting-0.2.0 → workforce_plotting-0.3.0}/src/workforce_plotting.egg-info/dependency_links.txt +0 -0
  21. {workforce_plotting-0.2.0 → workforce_plotting-0.3.0}/src/workforce_plotting.egg-info/requires.txt +0 -0
  22. {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)
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "workforce-plotting"
7
- version = "0.2.0"
7
+ version = "0.3.0"
8
8
  description = "Simple plotnine figures with Opportunity Insights styling."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -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"]