workforce-plotting 0.2.0__py3-none-any.whl → 0.3.0__py3-none-any.whl
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/__init__.py +2 -2
- workforce_plotting/figures.py +334 -49
- workforce_plotting-0.3.0.dist-info/METADATA +242 -0
- workforce_plotting-0.3.0.dist-info/RECORD +7 -0
- workforce_plotting-0.2.0.dist-info/METADATA +0 -135
- workforce_plotting-0.2.0.dist-info/RECORD +0 -7
- {workforce_plotting-0.2.0.dist-info → workforce_plotting-0.3.0.dist-info}/WHEEL +0 -0
- {workforce_plotting-0.2.0.dist-info → workforce_plotting-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {workforce_plotting-0.2.0.dist-info → workforce_plotting-0.3.0.dist-info}/top_level.txt +0 -0
workforce_plotting/__init__.py
CHANGED
|
@@ -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"]
|
workforce_plotting/figures.py
CHANGED
|
@@ -1,41 +1,92 @@
|
|
|
1
|
-
"""Workforce plots, shared styling, and the Opportunity Insights
|
|
1
|
+
"""Workforce plots, shared paper styling, and the Opportunity Insights palette."""
|
|
2
|
+
|
|
3
|
+
from math import pi, sqrt
|
|
2
4
|
|
|
3
5
|
import pandas as pd
|
|
4
6
|
import plotnine as pn
|
|
5
|
-
from matplotlib import font_manager
|
|
6
7
|
from oi_tools.figures import OIColors as WFColors, theme_oi
|
|
7
8
|
|
|
8
9
|
PALETTE = [WFColors.TEAL, WFColors.ORANGE, WFColors.NAVY, WFColors.PURPLE,
|
|
9
10
|
WFColors.BLUE, WFColors.RED, WFColors.DARK_GREEN]
|
|
10
11
|
|
|
11
12
|
|
|
12
|
-
def theme_workforce(base_size=12, base_family=
|
|
13
|
-
"""
|
|
13
|
+
def theme_workforce(base_size=12, base_family="Times New Roman", width=8, height=5):
|
|
14
|
+
"""Create the shared paper theme for a new or existing plot.
|
|
15
|
+
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
base_size : float, default 12
|
|
19
|
+
Size of axis text, axis titles, and legend text, in points.
|
|
20
|
+
base_family : str, default "Times New Roman"
|
|
21
|
+
Font family. Install this font locally, or choose another installed font,
|
|
22
|
+
such as "DejaVu Sans".
|
|
23
|
+
width, height : float, default 8, 5
|
|
24
|
+
Exported figure dimensions in inches.
|
|
25
|
+
|
|
26
|
+
Returns
|
|
27
|
+
-------
|
|
28
|
+
plotnine.theme
|
|
29
|
+
Add to a plot with ``p + theme_workforce(...)``. The plot constructors
|
|
30
|
+
already apply this theme; call it again only to override their defaults.
|
|
14
31
|
|
|
15
|
-
|
|
16
|
-
|
|
32
|
+
Notes
|
|
33
|
+
-----
|
|
34
|
+
Defaults include gray 0.54-point axes, five-point ticks, 10-point axis-title
|
|
35
|
+
padding, 2% outer margins, no grid, and a transparent vertical legend inside
|
|
36
|
+
the upper-left corner. Titles can be added with ``pn.labs(title=...)``.
|
|
37
|
+
|
|
38
|
+
Examples
|
|
39
|
+
--------
|
|
40
|
+
>>> p = plot_lines(df, x="year", y="earnings")
|
|
41
|
+
>>> p = p + theme_workforce(width=10, height=6)
|
|
17
42
|
"""
|
|
18
|
-
if base_family is None:
|
|
19
|
-
base_family = ("Lucida Sans" if "Lucida Sans" in font_manager.get_font_names()
|
|
20
|
-
else "DejaVu Sans")
|
|
21
43
|
return theme_oi(base_size, base_family, width, height) + pn.theme(
|
|
44
|
+
text=pn.element_text(linespacing=1.2),
|
|
22
45
|
axis_line=pn.element_line(color="#A0A0A0", size=0.54),
|
|
23
46
|
axis_ticks=pn.element_line(color="#A0A0A0", size=0.54),
|
|
47
|
+
axis_ticks_length=5,
|
|
24
48
|
axis_text=pn.element_text(color="black", size=base_size),
|
|
25
|
-
axis_title=pn.element_text(size=base_size
|
|
26
|
-
axis_title_y=pn.element_text(margin={"r":
|
|
27
|
-
axis_title_x=pn.element_text(margin={"t":
|
|
49
|
+
axis_title=pn.element_text(size=base_size),
|
|
50
|
+
axis_title_y=pn.element_text(margin={"r": 10}),
|
|
51
|
+
axis_title_x=pn.element_text(margin={"t": 10}),
|
|
52
|
+
legend_position="inside",
|
|
53
|
+
legend_position_inside=(0.03, 0.97),
|
|
54
|
+
legend_justification_inside=(0, 1),
|
|
55
|
+
legend_direction="vertical",
|
|
56
|
+
legend_background=pn.element_blank(),
|
|
28
57
|
legend_text=pn.element_text(size=base_size),
|
|
29
58
|
legend_title=pn.element_blank(),
|
|
30
|
-
|
|
31
|
-
plot_margin_right=0.015,
|
|
32
|
-
plot_margin_bottom=0.025,
|
|
59
|
+
plot_margin=0.02,
|
|
33
60
|
panel_grid=pn.element_blank(),
|
|
34
61
|
axis_ticks_minor=pn.element_blank(),
|
|
35
62
|
legend_key=pn.element_blank(),
|
|
36
63
|
)
|
|
37
64
|
|
|
38
65
|
|
|
66
|
+
def dollars(values):
|
|
67
|
+
"""Format axis tick values supplied in dollars as $1K, $2.5K, and $0.
|
|
68
|
+
|
|
69
|
+
Parameters
|
|
70
|
+
----------
|
|
71
|
+
values : iterable of numbers
|
|
72
|
+
Values in dollars, not values already divided by 1,000. Plotnine supplies
|
|
73
|
+
these values automatically when this function is used as a scale labeler.
|
|
74
|
+
|
|
75
|
+
Returns
|
|
76
|
+
-------
|
|
77
|
+
list of str
|
|
78
|
+
Labels in thousands of dollars, with negative values written as -$1K.
|
|
79
|
+
This changes labels only; it does not change the plotted data.
|
|
80
|
+
|
|
81
|
+
Examples
|
|
82
|
+
--------
|
|
83
|
+
>>> dollars([-1000, 0, 2500])
|
|
84
|
+
['-$1K', '$0', '$2.5K']
|
|
85
|
+
>>> p = p + pn.scale_y_continuous(labels=dollars)
|
|
86
|
+
"""
|
|
87
|
+
return [f"{'-' if v < 0 else ''}${abs(v) / 1000:g}K" if v else "$0" for v in values]
|
|
88
|
+
|
|
89
|
+
|
|
39
90
|
def _data(data):
|
|
40
91
|
"""Keep pandas inputs; convert Polars using its public conversion method."""
|
|
41
92
|
if isinstance(data, pd.DataFrame):
|
|
@@ -48,11 +99,63 @@ def _interval(lower, upper):
|
|
|
48
99
|
raise ValueError("Supply both lower and upper interval columns, or neither.")
|
|
49
100
|
|
|
50
101
|
|
|
51
|
-
def plot_lines(data, *, x, y, series=None, lower=None, upper=None, points=True
|
|
52
|
-
|
|
102
|
+
def plot_lines(data, *, x, y, series=None, lower=None, upper=None, points=True,
|
|
103
|
+
point_size=3.75, ci_width=0.54, cap_width=0.08):
|
|
104
|
+
"""Connect values in x order, optionally adding dots and confidence intervals.
|
|
105
|
+
|
|
106
|
+
Use for event studies, earnings trajectories, or model-complexity comparisons.
|
|
107
|
+
For unconnected estimates, use ``plot_scatter`` or ``plot_intervals`` instead.
|
|
108
|
+
|
|
109
|
+
Parameters
|
|
110
|
+
----------
|
|
111
|
+
data : pandas.DataFrame or polars.DataFrame
|
|
112
|
+
Prepared values, with one row per point. Column names are passed as
|
|
113
|
+
strings below; do not pass Series objects or arrays in their place.
|
|
114
|
+
x : str
|
|
115
|
+
Column for horizontal positions, such as "year" or "event_year".
|
|
116
|
+
y : str
|
|
117
|
+
Column containing the values to plot, such as "earnings" or "effect".
|
|
118
|
+
series : str or None, default None
|
|
119
|
+
Discrete group column, such as "estimator". Each group gets its own
|
|
120
|
+
connected line, color, and legend entry. None draws a single teal line.
|
|
121
|
+
Use ordered categories to control group order in the legend.
|
|
122
|
+
lower, upper : str or None, default None
|
|
123
|
+
Columns containing the lower and upper confidence bounds for y. Supply
|
|
124
|
+
both names together, or omit both. Bounds are drawn as supplied: this
|
|
125
|
+
function does not calculate standard errors or confidence intervals.
|
|
126
|
+
points : bool, default True
|
|
127
|
+
Draw dots on the line. False draws lines without dots.
|
|
128
|
+
point_size : float, default 3.75
|
|
129
|
+
Dot size in plotnine marker-size units, not a literal diameter in points.
|
|
130
|
+
Has no effect when points=False. Legend dots are 25% larger.
|
|
131
|
+
ci_width : float, default 0.54
|
|
132
|
+
Confidence-interval stroke width in points in the exported figure.
|
|
133
|
+
Changing this does not change dot size. Ignored without bounds.
|
|
134
|
+
cap_width : float, default 0.08
|
|
135
|
+
Total cap width in x-axis units, not points; zero removes caps. On a
|
|
136
|
+
discrete x-axis, one unit is the spacing between categories. The visible
|
|
137
|
+
cap length depends on the axis scale and figure dimensions.
|
|
138
|
+
|
|
139
|
+
Returns
|
|
140
|
+
-------
|
|
141
|
+
plotnine.ggplot
|
|
142
|
+
Editable plot. Add titles with ``+ pn.labs(title=..., x=..., y=...)``
|
|
143
|
+
and save with ``p.save("figure.png", dpi=180)``.
|
|
53
144
|
|
|
54
|
-
|
|
55
|
-
|
|
145
|
+
Notes
|
|
146
|
+
-----
|
|
147
|
+
Lines connect points in ascending x order within each group. Include a row
|
|
148
|
+
with a missing y value to break a line; omitting a row connects the remaining
|
|
149
|
+
points. No treatment-timing line or zero-effect line is added automatically.
|
|
150
|
+
The function does not fit a model or otherwise estimate the supplied values.
|
|
151
|
+
|
|
152
|
+
Examples
|
|
153
|
+
--------
|
|
154
|
+
For df with columns event_year, effect, lower, upper, and estimator:
|
|
155
|
+
|
|
156
|
+
>>> p = plot_lines(df, x="event_year", y="effect", series="estimator",
|
|
157
|
+
... lower="lower", upper="upper")
|
|
158
|
+
>>> p = p + pn.labs(title="Earnings Effects", x="Event Year", y="Effect")
|
|
56
159
|
"""
|
|
57
160
|
_interval(lower, upper)
|
|
58
161
|
mapping = pn.aes(x=x, y=y, group=1)
|
|
@@ -60,23 +163,76 @@ def plot_lines(data, *, x, y, series=None, lower=None, upper=None, points=True):
|
|
|
60
163
|
mapping.update(color=series, group=series)
|
|
61
164
|
p = pn.ggplot(_data(data), mapping)
|
|
62
165
|
if lower is not None:
|
|
63
|
-
p += pn.geom_errorbar(pn.aes(ymin=lower, ymax=upper
|
|
64
|
-
size=
|
|
65
|
-
|
|
166
|
+
p += pn.geom_errorbar(pn.aes(ymin=lower, ymax=upper, width=cap_width),
|
|
167
|
+
size=ci_width / sqrt(pi), show_legend=False,
|
|
168
|
+
**({} if series else {"color": WFColors.TEAL}))
|
|
169
|
+
p += pn.geom_line(size=0.914, show_legend=not points,
|
|
170
|
+
**({} if series else {"color": WFColors.TEAL}))
|
|
66
171
|
if points:
|
|
67
|
-
p += pn.geom_point(size=
|
|
172
|
+
p += pn.geom_point(size=point_size, stroke=0,
|
|
173
|
+
**({} if series else {"color": WFColors.TEAL}))
|
|
68
174
|
if series is not None:
|
|
69
175
|
p += pn.scale_color_manual(values=PALETTE)
|
|
176
|
+
if points:
|
|
177
|
+
p += pn.guides(color=pn.guide_legend(override_aes={"size": point_size * 1.25}))
|
|
70
178
|
return p + theme_workforce()
|
|
71
179
|
|
|
72
180
|
|
|
73
181
|
def plot_validation(data, *, x, y, x_lower=None, x_upper=None,
|
|
74
|
-
y_lower=None, y_upper=None, gaps=None, limits=None
|
|
75
|
-
|
|
182
|
+
y_lower=None, y_upper=None, gaps=None, limits=None,
|
|
183
|
+
point_size=3.75, ci_width=0.54, cap_width=0):
|
|
184
|
+
"""Compare paired estimates using an identity line and common axis limits.
|
|
185
|
+
|
|
186
|
+
Each row is one comparison, for example a site's DML and RCT estimates.
|
|
187
|
+
The identity line is y=x: points on it represent exact agreement. Unlike
|
|
188
|
+
``plot_scatter``, both axes share numeric limits. The panel remains rectangular;
|
|
189
|
+
equal physical scaling on the two axes is not imposed.
|
|
190
|
+
|
|
191
|
+
Parameters
|
|
192
|
+
----------
|
|
193
|
+
data : pandas.DataFrame or polars.DataFrame
|
|
194
|
+
Prepared paired estimates, with one row per point.
|
|
195
|
+
x : str
|
|
196
|
+
Horizontal estimate column, for example "dml_effect".
|
|
197
|
+
y : str
|
|
198
|
+
Vertical estimate column, for example "rct_effect".
|
|
199
|
+
x_lower, x_upper : str or None, default None
|
|
200
|
+
Columns containing lower and upper bounds for x (horizontal intervals).
|
|
201
|
+
Supply both names together, or omit both.
|
|
202
|
+
y_lower, y_upper : str or None, default None
|
|
203
|
+
Columns containing lower and upper bounds for y (vertical intervals).
|
|
204
|
+
Supply both names together, or omit both. All bounds are drawn as
|
|
205
|
+
supplied; the function does not calculate uncertainty.
|
|
206
|
+
gaps : {None, "horizontal", "vertical"}, default None
|
|
207
|
+
Optional orange segments from each point to the identity line.
|
|
208
|
+
"horizontal" connects (x, y) to (y, y), holding y fixed.
|
|
209
|
+
"vertical" connects (x, y) to (x, x), holding x fixed.
|
|
210
|
+
None draws no gap segments.
|
|
211
|
+
limits : tuple of two numbers or None, default None
|
|
212
|
+
Shared (minimum, maximum) view limits for both axes. None uses the
|
|
213
|
+
smallest and largest supplied estimates and interval endpoints.
|
|
214
|
+
Explicit view limits clip the display without dropping input rows.
|
|
215
|
+
point_size : float, default 3.75
|
|
216
|
+
Dot size in plotnine marker-size units, not a literal diameter in points.
|
|
217
|
+
ci_width : float, default 0.54
|
|
218
|
+
Interval stroke width in points in the exported figure, independent
|
|
219
|
+
of dot size. Ignored when no interval bounds are supplied.
|
|
220
|
+
cap_width : float, default 0
|
|
221
|
+
Cap length in axis units, not points; zero removes caps. It is measured
|
|
222
|
+
in y units for horizontal intervals and x units for vertical intervals.
|
|
76
223
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
224
|
+
Returns
|
|
225
|
+
-------
|
|
226
|
+
plotnine.ggplot
|
|
227
|
+
Editable plot; add labels with ``+ pn.labs(...)`` or save with ``p.save``.
|
|
228
|
+
The function does not fit a regression or estimate effects.
|
|
229
|
+
|
|
230
|
+
Examples
|
|
231
|
+
--------
|
|
232
|
+
For df with dml_effect, rct_effect, rct_lower, and rct_upper columns:
|
|
233
|
+
|
|
234
|
+
>>> p = plot_validation(df, x="dml_effect", y="rct_effect",
|
|
235
|
+
... y_lower="rct_lower", y_upper="rct_upper")
|
|
80
236
|
"""
|
|
81
237
|
_interval(x_lower, x_upper)
|
|
82
238
|
_interval(y_lower, y_upper)
|
|
@@ -97,20 +253,49 @@ def plot_validation(data, *, x, y, x_lower=None, x_upper=None,
|
|
|
97
253
|
elif gaps == "horizontal":
|
|
98
254
|
p += pn.geom_segment(pn.aes(xend=y, yend=y), color=WFColors.ORANGE, size=0.5, alpha=0.8)
|
|
99
255
|
if x_lower is not None:
|
|
100
|
-
p += pn.geom_errorbarh(pn.aes(xmin=x_lower, xmax=x_upper
|
|
101
|
-
color=WFColors.TEAL, size=
|
|
256
|
+
p += pn.geom_errorbarh(pn.aes(xmin=x_lower, xmax=x_upper, height=cap_width),
|
|
257
|
+
color=WFColors.TEAL, size=ci_width / sqrt(pi), alpha=0.5)
|
|
102
258
|
if y_lower is not None:
|
|
103
|
-
p += pn.geom_errorbar(pn.aes(ymin=y_lower, ymax=y_upper
|
|
104
|
-
color=WFColors.TEAL, size=
|
|
105
|
-
return (p + pn.geom_point(color=WFColors.TEAL, size=
|
|
259
|
+
p += pn.geom_errorbar(pn.aes(ymin=y_lower, ymax=y_upper, width=cap_width),
|
|
260
|
+
color=WFColors.TEAL, size=ci_width / sqrt(pi), alpha=0.5)
|
|
261
|
+
return (p + pn.geom_point(color=WFColors.TEAL, size=point_size, stroke=0)
|
|
106
262
|
+ pn.coord_cartesian(xlim=limits, ylim=limits) + theme_workforce())
|
|
107
263
|
|
|
108
264
|
|
|
109
265
|
def plot_bars(data, *, x, y, series=None):
|
|
110
|
-
"""
|
|
266
|
+
"""Draw supplied values as bars, optionally grouped side by side.
|
|
267
|
+
|
|
268
|
+
Use for RMSE by specification or other already-calculated category summaries.
|
|
269
|
+
This function draws values directly: it does not count rows, average values,
|
|
270
|
+
or calculate confidence intervals.
|
|
271
|
+
|
|
272
|
+
Parameters
|
|
273
|
+
----------
|
|
274
|
+
data : pandas.DataFrame or polars.DataFrame
|
|
275
|
+
One row per bar, containing the category and its already-calculated value.
|
|
276
|
+
x : str
|
|
277
|
+
Category column, for example "specification". Use an ordered categorical
|
|
278
|
+
column or ``+ pn.scale_x_discrete(limits=[...])`` to control its order.
|
|
279
|
+
y : str
|
|
280
|
+
Numeric bar-height column, for example "rmse".
|
|
281
|
+
series : str or None, default None
|
|
282
|
+
Discrete group column. Groups receive different colors and appear side
|
|
283
|
+
by side within each x category, with a legend identifying them.
|
|
284
|
+
None draws a single teal series.
|
|
111
285
|
|
|
112
|
-
|
|
113
|
-
|
|
286
|
+
Returns
|
|
287
|
+
-------
|
|
288
|
+
plotnine.ggplot
|
|
289
|
+
Editable plot. Add ``+ pn.coord_flip()`` for horizontal bars, labels with
|
|
290
|
+
``+ pn.labs(...)``, or optional dollar labels with
|
|
291
|
+
``+ pn.scale_y_continuous(labels=dollars)``. Save with ``p.save``.
|
|
292
|
+
|
|
293
|
+
Examples
|
|
294
|
+
--------
|
|
295
|
+
For df with specification and rmse columns:
|
|
296
|
+
|
|
297
|
+
>>> p = plot_bars(df, x="specification", y="rmse")
|
|
298
|
+
>>> p = p + pn.labs(title="RMSE by Specification", x="", y="RMSE")
|
|
114
299
|
"""
|
|
115
300
|
mapping = pn.aes(x=x, y=y, group=1)
|
|
116
301
|
if series is not None:
|
|
@@ -122,37 +307,137 @@ def plot_bars(data, *, x, y, series=None):
|
|
|
122
307
|
return p + theme_workforce()
|
|
123
308
|
|
|
124
309
|
|
|
125
|
-
def plot_scatter(data, *, x, y, series=None, fitted=None
|
|
126
|
-
|
|
310
|
+
def plot_scatter(data, *, x, y, series=None, fitted=None, lower=None, upper=None,
|
|
311
|
+
point_size=3.75, ci_width=0.54, cap_width=0):
|
|
312
|
+
"""Draw unconnected points, optionally with intervals and a supplied curve.
|
|
313
|
+
|
|
314
|
+
Use to show a relationship between two variables or cohort estimates with a
|
|
315
|
+
separate fitted curve. Unlike ``plot_validation``, no identity line is added
|
|
316
|
+
and the x and y axes choose their ranges independently.
|
|
317
|
+
|
|
318
|
+
Parameters
|
|
319
|
+
----------
|
|
320
|
+
data : pandas.DataFrame or polars.DataFrame
|
|
321
|
+
Prepared observations or estimates, with one row per point.
|
|
322
|
+
x : str
|
|
323
|
+
Horizontal-position column, for example "placement_rate" or "year".
|
|
324
|
+
y : str
|
|
325
|
+
Vertical-value column, for example "effect".
|
|
326
|
+
series : str or None, default None
|
|
327
|
+
Discrete group column, such as "city". Groups get distinct colors and
|
|
328
|
+
legend entries. None draws teal dots with a navy fitted curve, if supplied.
|
|
329
|
+
fitted : pandas.DataFrame, polars.DataFrame, or None, default None
|
|
330
|
+
Separate table of already-calculated curve coordinates. It must use
|
|
331
|
+
the same x and y column names and, when series is supplied, the same
|
|
332
|
+
group column name. Its rows need not match the number of observations.
|
|
333
|
+
Points in this table are connected in x order within each group.
|
|
334
|
+
This function never fits, interpolates, or smooths a statistical model;
|
|
335
|
+
None draws no curve. The observed dots themselves are not connected.
|
|
336
|
+
lower, upper : str or None, default None
|
|
337
|
+
Columns in data containing lower and upper confidence bounds for y.
|
|
338
|
+
Supply both names together, or omit both. These intervals describe the
|
|
339
|
+
supplied point estimates; they are not a confidence band for the curve.
|
|
340
|
+
point_size : float, default 3.75
|
|
341
|
+
Dot size in plotnine marker-size units, not a literal diameter in points.
|
|
342
|
+
Legend dots are 25% larger.
|
|
343
|
+
ci_width : float, default 0.54
|
|
344
|
+
Interval stroke width in points in the exported figure, independent
|
|
345
|
+
of dot size. Ignored without bounds.
|
|
346
|
+
cap_width : float, default 0
|
|
347
|
+
Total interval-cap width in x-axis units, not points; zero removes caps.
|
|
348
|
+
The visible length depends on the axis scale and figure dimensions.
|
|
127
349
|
|
|
128
|
-
|
|
129
|
-
|
|
350
|
+
Returns
|
|
351
|
+
-------
|
|
352
|
+
plotnine.ggplot
|
|
353
|
+
Editable plot; add labels with ``+ pn.labs(...)`` or save with ``p.save``.
|
|
354
|
+
All estimates, fitted values, and bounds must be calculated beforehand.
|
|
355
|
+
|
|
356
|
+
Examples
|
|
357
|
+
--------
|
|
358
|
+
For df with year, effect, lower, and upper columns, and curve_df with year
|
|
359
|
+
and effect columns containing previously calculated fitted values:
|
|
360
|
+
|
|
361
|
+
>>> p = plot_scatter(df, x="year", y="effect", lower="lower", upper="upper",
|
|
362
|
+
... fitted=curve_df)
|
|
130
363
|
"""
|
|
364
|
+
_interval(lower, upper)
|
|
131
365
|
mapping = pn.aes(x=x, y=y, group=1)
|
|
132
366
|
if series is not None:
|
|
133
367
|
mapping.update(color=series, group=series)
|
|
134
368
|
p = pn.ggplot(_data(data), mapping)
|
|
369
|
+
if lower is not None:
|
|
370
|
+
p += pn.geom_errorbar(pn.aes(ymin=lower, ymax=upper, width=cap_width),
|
|
371
|
+
size=ci_width / sqrt(pi), show_legend=False,
|
|
372
|
+
**({} if series else {"color": WFColors.TEAL}))
|
|
135
373
|
if fitted is not None:
|
|
136
|
-
p += pn.geom_line(data=_data(fitted), size=0.914,
|
|
374
|
+
p += pn.geom_line(data=_data(fitted), size=0.914, show_legend=False,
|
|
137
375
|
**({} if series else {"color": WFColors.NAVY}))
|
|
138
|
-
p += pn.geom_point(size=
|
|
376
|
+
p += pn.geom_point(size=point_size, stroke=0, **({} if series else {"color": WFColors.TEAL}))
|
|
139
377
|
if series is not None:
|
|
140
378
|
p += pn.scale_color_manual(values=PALETTE)
|
|
379
|
+
p += pn.guides(color=pn.guide_legend(override_aes={"size": point_size * 1.25}))
|
|
141
380
|
return p + theme_workforce()
|
|
142
381
|
|
|
143
382
|
|
|
144
|
-
def plot_intervals(data, *, x, y, lower, upper, series=None
|
|
145
|
-
|
|
383
|
+
def plot_intervals(data, *, x, y, lower, upper, series=None,
|
|
384
|
+
point_size=3.75, ci_width=0.54, cap_width=0):
|
|
385
|
+
"""Draw unconnected estimates and confidence intervals across categories.
|
|
386
|
+
|
|
387
|
+
Use for treatment effects by program/site or coefficient plots. Unlike
|
|
388
|
+
``plot_lines``, estimates are not connected, and no fitted curve is added.
|
|
389
|
+
|
|
390
|
+
Parameters
|
|
391
|
+
----------
|
|
392
|
+
data : pandas.DataFrame or polars.DataFrame
|
|
393
|
+
Prepared estimates, with one row per category (or category/group pair).
|
|
394
|
+
x : str
|
|
395
|
+
Category column, such as "program" or "site". Ordered categories control
|
|
396
|
+
the display order; otherwise plotnine uses its normal category ordering.
|
|
397
|
+
y : str
|
|
398
|
+
Numeric estimate column, such as "effect".
|
|
399
|
+
lower, upper : str
|
|
400
|
+
Required column names for the lower and upper confidence bounds for y.
|
|
401
|
+
Supply the actual endpoints, not standard errors. The function does
|
|
402
|
+
not calculate bounds or choose a confidence level.
|
|
403
|
+
series : str or None, default None
|
|
404
|
+
Discrete group column, such as "estimator". Groups receive different
|
|
405
|
+
colors and are offset side by side within each category so dots and
|
|
406
|
+
intervals do not overlap exactly. None draws one teal series.
|
|
407
|
+
point_size : float, default 3.75
|
|
408
|
+
Dot size in plotnine marker-size units, not a literal diameter in points.
|
|
409
|
+
Legend dots are 25% larger.
|
|
410
|
+
ci_width : float, default 0.54
|
|
411
|
+
Interval stroke width in points in the exported figure, independent
|
|
412
|
+
of dot size.
|
|
413
|
+
cap_width : float, default 0
|
|
414
|
+
Cap width in category-axis units, not points; zero removes caps.
|
|
415
|
+
One unit is the spacing between categories on a discrete axis.
|
|
416
|
+
Caps are narrowed along with groups when series are offset side by side.
|
|
417
|
+
|
|
418
|
+
Returns
|
|
419
|
+
-------
|
|
420
|
+
plotnine.ggplot
|
|
421
|
+
Editable plot. Add ``+ pn.coord_flip()`` for a horizontal coefficient
|
|
422
|
+
plot, labels with ``+ pn.labs(...)``, or save with ``p.save``.
|
|
423
|
+
|
|
424
|
+
Examples
|
|
425
|
+
--------
|
|
426
|
+
For df with program, effect, lower, and upper columns:
|
|
146
427
|
|
|
147
|
-
|
|
148
|
-
|
|
428
|
+
>>> p = plot_intervals(df, x="program", y="effect", lower="lower", upper="upper")
|
|
429
|
+
>>> p = p + pn.coord_flip()
|
|
149
430
|
"""
|
|
150
431
|
mapping = pn.aes(x=x, y=y, ymin=lower, ymax=upper)
|
|
151
432
|
if series is not None:
|
|
152
433
|
mapping.update(color=series, group=series)
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
434
|
+
dodge = pn.position_dodge(width=0.5)
|
|
435
|
+
p = (pn.ggplot(_data(data), mapping)
|
|
436
|
+
+ pn.geom_errorbar(pn.aes(width=cap_width), position=dodge, size=ci_width / sqrt(pi),
|
|
437
|
+
show_legend=False, **({} if series else {"color": WFColors.TEAL}))
|
|
438
|
+
+ pn.geom_point(position=dodge, size=point_size, stroke=0,
|
|
439
|
+
**({} if series else {"color": WFColors.TEAL})))
|
|
156
440
|
if series is not None:
|
|
157
441
|
p += pn.scale_color_manual(values=PALETTE)
|
|
442
|
+
p += pn.guides(color=pn.guide_legend(override_aes={"size": point_size * 1.25}))
|
|
158
443
|
return p + theme_workforce()
|
|
@@ -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,7 @@
|
|
|
1
|
+
workforce_plotting/__init__.py,sha256=WMPL6dhkaVLCxJPSb516VWcVSjsI2R_UMYIjwfOe4R4,344
|
|
2
|
+
workforce_plotting/figures.py,sha256=Dp9NrPWg5X0SxGmJV5k_1wEcqaKbbxj5sciUX4-NFIQ,20145
|
|
3
|
+
workforce_plotting-0.3.0.dist-info/licenses/LICENSE,sha256=vIUOXKFifh8J0Pb7KgpB0HKgiKPfoMcz9zAqUHKl8mo,1076
|
|
4
|
+
workforce_plotting-0.3.0.dist-info/METADATA,sha256=z0AqJOr6HBGGss21eIkXQNQa3LxsLWS6RkFZnczsHEM,9803
|
|
5
|
+
workforce_plotting-0.3.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
6
|
+
workforce_plotting-0.3.0.dist-info/top_level.txt,sha256=VBDPtAcqz3Z46cgEzdq8GGm8YIFQtkNcI1Zv8pqqcu4,19
|
|
7
|
+
workforce_plotting-0.3.0.dist-info/RECORD,,
|
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: workforce-plotting
|
|
3
|
-
Version: 0.2.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 Opportunity Insights style. 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
|
-
## Functions
|
|
55
|
-
|
|
56
|
-
| Function | Use |
|
|
57
|
-
|---|---|
|
|
58
|
-
| `plot_lines(data, x=..., y=..., series=...)` | Event studies, trajectories, cohort trends, model complexity |
|
|
59
|
-
| `plot_validation(data, x=..., y=...)` | RCT versus digital-twin estimates with an identity line |
|
|
60
|
-
| `plot_bars(data, x=..., y=..., series=...)` | Already-calculated values by category |
|
|
61
|
-
| `plot_scatter(data, x=..., y=..., fitted=...)` | Points with an optional precomputed fitted line |
|
|
62
|
-
| `plot_intervals(data, x=..., y=..., lower=..., upper=...)` | Estimates and supplied confidence intervals |
|
|
63
|
-
| `theme_workforce()` | Shared white background, gray axes, sans-serif type, no grid |
|
|
64
|
-
|
|
65
|
-
`plot_lines` accepts `lower` and `upper` columns and `points=False`.
|
|
66
|
-
`plot_validation` accepts `x_lower`, `x_upper`, `y_lower`, `y_upper`,
|
|
67
|
-
`gaps="horizontal"` or `"vertical"`, and a common `(min, max)` pair of `limits`.
|
|
68
|
-
The identity line has slope one in data units; the panel follows the figure's
|
|
69
|
-
aspect ratio. Other functions accept `series` for discrete groups. Use ordered
|
|
70
|
-
categorical columns to control color/legend order. Colors are teal, orange, navy,
|
|
71
|
-
purple, blue, red, and dark green from the public `oi_tools.figures.OIColors` API.
|
|
72
|
-
For a different mapping, add a plotnine manual color or fill scale.
|
|
73
|
-
|
|
74
|
-
```python
|
|
75
|
-
import plotnine as pn
|
|
76
|
-
from workforce_plotting import plot_lines
|
|
77
|
-
|
|
78
|
-
p = plot_lines(df, x="year", y="earnings", series="group")
|
|
79
|
-
p += pn.labs(x="Years since enrollment", y="Mean annual earnings")
|
|
80
|
-
p.save("earnings.svg")
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
Dataframes should be tidy: one row per plotted point, with separate columns for
|
|
84
|
-
x, y, and an optional series. Lines connect points in x order within each series.
|
|
85
|
-
Include a row with a missing y where a line should break; simply omitting a row
|
|
86
|
-
connects the remaining observations. Fitted lines use the same column names as
|
|
87
|
-
the scatter data. No smoothing, RMSE calculation, confidence-interval estimation,
|
|
88
|
-
or automatic saving happens inside the plotting functions.
|
|
89
|
-
|
|
90
|
-
Add `pn.coord_flip()` for horizontal bars/intervals; use ordinary plotnine scales
|
|
91
|
-
for dollars, percentages, limits, or log axes. Fonts, dimensions, and text size
|
|
92
|
-
use Lucida Sans when installed and DejaVu Sans otherwise. They can be overridden with `theme_workforce(base_family=..., base_size=..., width=..., height=...)`.
|
|
93
|
-
Titles, logos, notes, and paper-specific annotations belong in the example/caller.
|
|
94
|
-
|
|
95
|
-
## Styling your own plots
|
|
96
|
-
|
|
97
|
-
Use the theme and colors without using the plot constructors. `WFColors` is an
|
|
98
|
-
alias of `oi_tools.figures.OIColors`, and `theme_workforce` builds on `theme_oi`.
|
|
99
|
-
The `oi-tools` dependency remains; you only need workforce imports in your code.
|
|
100
|
-
All plotting functions and styling live together in `workforce_plotting/figures.py`.
|
|
101
|
-
|
|
102
|
-
```python
|
|
103
|
-
import polars as pl
|
|
104
|
-
import plotnine as pn
|
|
105
|
-
from workforce_plotting import theme_workforce, WFColors
|
|
106
|
-
|
|
107
|
-
df = pl.DataFrame({"year": [2020, 2021, 2022], "earnings": [20000, 23000, 25000]})
|
|
108
|
-
p = (
|
|
109
|
-
pn.ggplot(df.to_pandas(), pn.aes(x="year", y="earnings"))
|
|
110
|
-
+ pn.geom_line(color=WFColors.TEAL)
|
|
111
|
-
+ pn.geom_point(color=WFColors.TEAL)
|
|
112
|
-
+ theme_workforce()
|
|
113
|
-
+ pn.labs(x="Year", y="Annual earnings")
|
|
114
|
-
)
|
|
115
|
-
p.save("earnings.svg")
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
Plot constructors accept Polars directly. When building a plotnine plot yourself,
|
|
119
|
-
the explicit conversion above works with the supported plotnine version.
|
|
120
|
-
|
|
121
|
-
## Editable examples
|
|
122
|
-
|
|
123
|
-
`examples/paper_examples.py` contains synthetic recipes for the paper/deck plot
|
|
124
|
-
families. These are invented values, not research results.
|
|
125
|
-
|
|
126
|
-
```bash
|
|
127
|
-
python examples/paper_examples.py outputs/examples
|
|
128
|
-
pytest
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
## Changes from 0.1
|
|
132
|
-
|
|
133
|
-
This is a fresh API. The old Matplotlib functions, plotting dataclasses, curve
|
|
134
|
-
fitting machinery, and `aipw_analysis` compatibility package have been removed.
|
|
135
|
-
Plot creation and saving are now separate. The public API consists of the five plot functions, `theme_workforce`, and `WFColors`.
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
workforce_plotting/__init__.py,sha256=Hu7rBJz6wdcIAn84s62-xTZFlz_bTee-3xIDd8u87s8,324
|
|
2
|
-
workforce_plotting/figures.py,sha256=YHXf-PC349SgBXOTJhI4GZZcvPjJmZvP2xEC0eMWMiQ,6978
|
|
3
|
-
workforce_plotting-0.2.0.dist-info/licenses/LICENSE,sha256=vIUOXKFifh8J0Pb7KgpB0HKgiKPfoMcz9zAqUHKl8mo,1076
|
|
4
|
-
workforce_plotting-0.2.0.dist-info/METADATA,sha256=llHdwfUYE9Osn3Mp6uAQvfzOCxxOP-rnyKyep6TsJK4,5309
|
|
5
|
-
workforce_plotting-0.2.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
6
|
-
workforce_plotting-0.2.0.dist-info/top_level.txt,sha256=VBDPtAcqz3Z46cgEzdq8GGm8YIFQtkNcI1Zv8pqqcu4,19
|
|
7
|
-
workforce_plotting-0.2.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|