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.
@@ -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"]
@@ -1,41 +1,92 @@
1
- """Workforce plots, shared styling, and the Opportunity Insights color palette."""
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=None, width=8, height=5):
13
- """White background, gray axes, no grid, and a bottom legend.
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
- Dimensions are in inches. Install Lucida Sans to match the reference deck.
16
- If unavailable, Matplotlib falls back to its default sans-serif font.
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, lineheight=1.8),
26
- axis_title_y=pn.element_text(margin={"r": 16}),
27
- axis_title_x=pn.element_text(margin={"t": 14}),
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
- plot_margin_top=0.044,
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
- """Lines with optional points and supplied confidence intervals.
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
- Use an ordered categorical series to control legend/color order. Add scales,
55
- reference lines, facets, and labels using ordinary plotnine layers.
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), width=0.08,
64
- size=0.4, **({} if series else {"color": WFColors.TEAL}))
65
- p += pn.geom_line(size=0.914, **({} if series else {"color": WFColors.TEAL}))
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=3.45, stroke=0, **({} if series else {"color": WFColors.TEAL}))
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
- """Compare estimates against a 45-degree identity line.
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
- x/y confidence bounds are optional. ``gaps`` may be 'vertical' (from y to x)
78
- or 'horizontal' (from x to y). Both axes use the same numeric limits.
79
- Default limits include all supplied estimates and interval endpoints.
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), height=0,
101
- color=WFColors.TEAL, size=0.5, alpha=0.5)
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), width=0,
104
- color=WFColors.TEAL, size=0.5, alpha=0.5)
105
- return (p + pn.geom_point(color=WFColors.TEAL, size=3.45, stroke=0)
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
- """Bars for already-calculated values; optional series are side by side.
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
- Set x to an ordered categorical column, or add scale_x_discrete(limits=...),
113
- to choose the category order. Add coord_flip() for horizontal bars.
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
- """Points, optionally with a separate dataframe of precomputed fitted values.
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
- ``fitted`` uses the same x, y, and (when specified) series column names.
129
- The function never fits or smooths a curve.
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=3.45, stroke=0, **({} if series else {"color": WFColors.TEAL}))
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
- """Points with supplied interval bounds, dodged when series are supplied.
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
- x is the category and y the estimate. Add coord_flip() for a horizontal
148
- coefficient plot. Interval bounds are used as supplied, without adjustment.
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
- p = pn.ggplot(_data(data), mapping) + pn.geom_pointrange(
154
- position=pn.position_dodge(width=0.5), size=0.5,
155
- **({} if series else {"color": WFColors.TEAL}))
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,,