goad-toolkit 0.2.13__tar.gz → 0.2.15__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 (125) hide show
  1. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.remember/logs/memory-2026-08-31.log +33 -0
  2. goad_toolkit-0.2.15/.remember/tmp/last-save-ts +1 -0
  3. goad_toolkit-0.2.15/.remember/tmp/last-summary-failure +1 -0
  4. goad_toolkit-0.2.15/CHANGELOG.md +90 -0
  5. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/PKG-INFO +1 -1
  6. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/docs/02-pipelines.md +9 -0
  7. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/docs/03-plot-composition.md +42 -6
  8. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/docs/08-api-reference.md +39 -5
  9. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/pyproject.toml +1 -1
  10. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/src/goad_toolkit/datatransforms.py +172 -0
  11. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/src/goad_toolkit/visualizer.py +205 -17
  12. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/tests/test_datatransforms.py +166 -0
  13. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/tests/test_visualizer.py +176 -1
  14. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/uv.lock +1 -1
  15. goad_toolkit-0.2.13/.remember/tmp/last-save-ts +0 -1
  16. goad_toolkit-0.2.13/.remember/tmp/last-summary-failure +0 -1
  17. goad_toolkit-0.2.13/.remember/tmp/save-session.pid +0 -1
  18. goad_toolkit-0.2.13/CHANGELOG.md +0 -37
  19. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/settings.local.json +0 -0
  20. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/.claude/settings.local.json +0 -0
  21. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/.gitignore +0 -0
  22. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/.python-version +0 -0
  23. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/CHANGELOG.md +0 -0
  24. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/MCP_SERVER.md +0 -0
  25. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/README.md +0 -0
  26. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/demo/linear.py +0 -0
  27. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/docs/01-goal-oriented-analysis.md +0 -0
  28. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/docs/02-pipelines.md +0 -0
  29. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/docs/03-plot-composition.md +0 -0
  30. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/docs/04-five-families.md +0 -0
  31. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/docs/05-distributions.md +0 -0
  32. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/docs/06-models-and-residuals.md +0 -0
  33. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/docs/07-visual-critique.md +0 -0
  34. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/docs/08-api-reference.md +0 -0
  35. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/docs/09-analysis-method.md +0 -0
  36. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/docs/10-teaching-path.md +0 -0
  37. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/docs/README.md +0 -0
  38. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/goad_mcp.py +0 -0
  39. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/img/distribution_fit.png +0 -0
  40. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/img/goaded.png +0 -0
  41. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/img/linear_results.png +0 -0
  42. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/img/null-distribution.png +0 -0
  43. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/img/residuals.png +0 -0
  44. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/img/zscores.png +0 -0
  45. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/pyproject.toml +0 -0
  46. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/src/goad_toolkit/__init__.py +0 -0
  47. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/src/goad_toolkit/analytics.py +0 -0
  48. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/src/goad_toolkit/cli.py +0 -0
  49. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/src/goad_toolkit/config.py +0 -0
  50. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/src/goad_toolkit/dataprocessor.py +0 -0
  51. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/src/goad_toolkit/datatransforms.py +0 -0
  52. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/src/goad_toolkit/distributions.py +0 -0
  53. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/src/goad_toolkit/filehandler.py +0 -0
  54. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/src/goad_toolkit/models.py +0 -0
  55. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/src/goad_toolkit/visualizer.py +0 -0
  56. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/tests/test_cli.py +0 -0
  57. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/tests/test_datatransforms.py +0 -0
  58. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/tests/test_distributions.py +0 -0
  59. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/tests/test_filehandler.py +0 -0
  60. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/tests/test_nulldistribution.py +0 -0
  61. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/tests/test_visualizer.py +0 -0
  62. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/uv.lock +0 -0
  63. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.github/workflows/release.yml +0 -0
  64. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.gitignore +0 -0
  65. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.python-version +0 -0
  66. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.remember/.gitignore +0 -0
  67. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.remember/archive.md +0 -0
  68. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.remember/logs/autonomous/save-170051.log +0 -0
  69. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.remember/logs/autonomous/save-170257.log +0 -0
  70. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.remember/logs/autonomous/save-172752.log +0 -0
  71. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.remember/logs/autonomous/save-172954.log +0 -0
  72. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.remember/logs/autonomous/save-174001.log +0 -0
  73. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.remember/logs/autonomous/save-174204.log +0 -0
  74. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.remember/logs/autonomous/save-174415.log +0 -0
  75. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.remember/logs/autonomous/save-174621.log +0 -0
  76. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.remember/logs/autonomous/save-183124.log +0 -0
  77. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.remember/logs/autonomous/save-183325.log +0 -0
  78. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.remember/logs/autonomous/save-183803.log +0 -0
  79. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.remember/logs/autonomous/save-184713.log +0 -0
  80. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.remember/logs/autonomous/save-184913.log +0 -0
  81. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.remember/logs/hook-errors.log +0 -0
  82. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.remember/logs/logs-2026-08.tar.gz +0 -0
  83. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.remember/now.md +0 -0
  84. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.remember/recent.md +0 -0
  85. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.remember/tmp/capture-alive +0 -0
  86. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.remember/tmp/capture-alive.d/16b4788a-5507-4216-98ec-06bac13a9aba +0 -0
  87. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.remember/tmp/capture-alive.d/309ee39a-6171-47ea-88cf-ee1ad9433f68 +0 -0
  88. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.remember/tmp/capture-alive.d/90574e63-97cd-4c11-87e5-23b656c56fd9 +0 -0
  89. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.remember/tmp/case-divergence +0 -0
  90. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.remember/tmp/last-ndc.ts +0 -0
  91. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.remember/tmp/last-save.json +0 -0
  92. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.remember/tmp/post-tool-ran +0 -0
  93. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.remember/tmp/session-slug +0 -0
  94. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.remember/today-2026-08-10.done.md +0 -0
  95. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.remember/today-2026-08-10.md +0 -0
  96. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/MCP_SERVER.md +0 -0
  97. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/README.md +0 -0
  98. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/demo/linear.py +0 -0
  99. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/docs/01-goal-oriented-analysis.md +0 -0
  100. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/docs/04-five-families.md +0 -0
  101. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/docs/05-distributions.md +0 -0
  102. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/docs/06-models-and-residuals.md +0 -0
  103. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/docs/07-visual-critique.md +0 -0
  104. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/docs/09-analysis-method.md +0 -0
  105. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/docs/10-teaching-path.md +0 -0
  106. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/docs/README.md +0 -0
  107. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/goad_mcp.py +0 -0
  108. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/img/distribution_fit.png +0 -0
  109. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/img/goaded.png +0 -0
  110. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/img/linear_results.png +0 -0
  111. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/img/null-distribution.png +0 -0
  112. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/img/residuals.png +0 -0
  113. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/img/zscores.png +0 -0
  114. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/src/goad_toolkit/__init__.py +0 -0
  115. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/src/goad_toolkit/analytics.py +0 -0
  116. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/src/goad_toolkit/cli.py +0 -0
  117. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/src/goad_toolkit/config.py +0 -0
  118. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/src/goad_toolkit/dataprocessor.py +0 -0
  119. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/src/goad_toolkit/distributions.py +0 -0
  120. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/src/goad_toolkit/filehandler.py +0 -0
  121. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/src/goad_toolkit/models.py +0 -0
  122. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/tests/test_cli.py +0 -0
  123. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/tests/test_distributions.py +0 -0
  124. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/tests/test_filehandler.py +0 -0
  125. {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/tests/test_nulldistribution.py +0 -0
@@ -342,3 +342,36 @@ RuntimeError: claude exited 1: Failed to authenticate: OAuth session expired and
342
342
  18:49:15 [haiku] call exited 1; tokens already spent are unknown — the failure carried no usage block, so this run's reported cost is lower than what it actually cost
343
343
  18:49:15 [haiku] ERROR: call-haiku error: claude exited 1: Failed to authenticate: OAuth session expired and could not be refreshed
344
344
  18:49:15 [haiku] failure 1/3 on this span — will retry next run
345
+ 21:09:54 [hook] session-start: PROJECT_DIR=/Users/rgrouls/code/courses/goad_toolkit PIPELINE_DIR=/Users/rgrouls/.claude/plugins/cache/claude-plugins-official/remember/0.20.0 REMEMBER_DIR=/Users/rgrouls/code/courses/goad_toolkit/.remember
346
+ 21:09:54 [hook] save-session: PROJECT_DIR=/Users/rgrouls/code/courses/goad_toolkit PIPELINE_DIR=/Users/rgrouls/.claude/plugins/cache/claude-plugins-official/remember/0.20.0 PYTHON=python3 REMEMBER_DIR=/Users/rgrouls/code/courses/goad_toolkit/.remember
347
+ 21:09:54 [force] bypassing cooldown + min msgs
348
+ 21:09:54 [hook] run-consolidation: PROJECT_DIR=/Users/rgrouls/code/courses/goad_toolkit PIPELINE_DIR=/Users/rgrouls/.claude/plugins/cache/claude-plugins-official/remember/0.20.0 PYTHON=python3 REMEMBER_DIR=/Users/rgrouls/code/courses/goad_toolkit/.remember
349
+ 21:09:54 [extract] session 309ee39a-6171-47ea-88cf-ee1ad9433f68
350
+ 21:09:54 [extract] 204 exchanges (10 human)
351
+ 21:09:54 [consolidation] start
352
+ 21:09:54 [haiku] calling (branch: main)
353
+ 21:09:54 [hook] session-start: PROJECT_DIR=/Users/rgrouls/code/courses/goad_toolkit PIPELINE_DIR=/Users/rgrouls/.claude/plugins/cache/claude-plugins-official/remember/0.20.0 REMEMBER_DIR=/Users/rgrouls/code/courses/goad_toolkit/.remember
354
+ 21:09:54 [hook] run-consolidation: PROJECT_DIR=/Users/rgrouls/code/courses/goad_toolkit PIPELINE_DIR=/Users/rgrouls/.claude/plugins/cache/claude-plugins-official/remember/0.20.0 PYTHON=python3 REMEMBER_DIR=/Users/rgrouls/code/courses/goad_toolkit/.remember
355
+ 21:09:54 [consolidation] another consolidation holds the lock, skip
356
+ 21:09:56 [haiku] WARNING: this CLI rejected --setting-sources (Failed to authenticate: OAuth session expired and could not be refreshed); retrying WITHOUT hook isolation so saves keep working. The nested call will run with your hooks registered — a hook that blocks it returns its block message as if it were the model's reply (#202).
357
+ 21:09:56 [haiku] WARNING: this CLI rejected --setting-sources (Failed to authenticate: OAuth session expired and could not be refreshed); retrying WITHOUT hook isolation so saves keep working. The nested call will run with your hooks registered — a hook that blocks it returns its block message as if it were the model's reply (#202).
358
+ 21:09:57 [haiku] call exited 1; tokens already spent are unknown — the failure carried no usage block, so this run's reported cost is lower than what it actually cost
359
+ 21:09:57 [haiku] call exited 1; tokens already spent are unknown — the failure carried no usage block, so this run's reported cost is lower than what it actually cost
360
+ 21:09:57 [haiku] ERROR: call-haiku error: claude exited 1: Failed to authenticate: OAuth session expired and could not be refreshed
361
+ 21:09:57 [haiku] failure 1/3 on this span — will retry next run
362
+ 21:09:57 [consolidation] ERROR: pipeline failed — Traceback (most recent call last):
363
+ File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/runpy.py", line 197, in _run_module_as_main
364
+ return _run_code(code, main_globals, None,
365
+ File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/runpy.py", line 87, in _run_code
366
+ exec(code, run_globals)
367
+ File "/Users/rgrouls/.claude/plugins/cache/claude-plugins-official/remember/0.20.0/pipeline/shell.py", line 683, in <module>
368
+ main()
369
+ File "/Users/rgrouls/.claude/plugins/cache/claude-plugins-official/remember/0.20.0/pipeline/shell.py", line 670, in main
370
+ cmd_consolidate(
371
+ File "/Users/rgrouls/.claude/plugins/cache/claude-plugins-official/remember/0.20.0/pipeline/shell.py", line 525, in cmd_consolidate
372
+ result = consolidate(staging_contents, recent, archive,
373
+ File "/Users/rgrouls/.claude/plugins/cache/claude-plugins-official/remember/0.20.0/pipeline/consolidate.py", line 324, in consolidate
374
+ result = call_haiku(prompt, timeout=180)
375
+ File "/Users/rgrouls/.claude/plugins/cache/claude-plugins-official/remember/0.20.0/pipeline/haiku.py", line 735, in call_haiku
376
+ raise RuntimeError(
377
+ RuntimeError: claude exited 1: Failed to authenticate: OAuth session expired and could not be refreshed
@@ -0,0 +1 @@
1
+ 1788203394
@@ -0,0 +1 @@
1
+ 309ee39a-6171-47ea-88cf-ee1ad9433f68:747 1
@@ -0,0 +1,90 @@
1
+ # Changelog
2
+
3
+ ## 0.2.15
4
+
5
+ ### Added
6
+
7
+ - **Per-call settings overrides.** Any keyword passed to `plot()` that names a
8
+ `PlotSettings` field (`title`, `xlabel`, `ylabel`, ...) overrides that field for that
9
+ call only, leaving the shared settings object untouched — the claim lives in the call,
10
+ the styling stays in the settings. On a layer whose figure already exists, the
11
+ axis-level fields (`title`, `xlabel`, `ylabel`, `xtick_rotation`) still apply.
12
+ - `FacetPlot`: the same plot once per level of a column — small multiples in one call,
13
+ with panel titles defaulting to the level names.
14
+ - `PlotSettings.sharex` / `sharey`: subplot grids share one scale, so panel shapes
15
+ compare honestly. `FacetPlot` is the main customer.
16
+ - A pipeline vocabulary for the counting-and-shares work notebooks kept doing in loose
17
+ cells: `Filter`, `SortValues`, `Head`, `CountValues`, `GroupAgg`, `Share`, `FlagDates`,
18
+ `MapValues`, `SubtractBaseline`. Output-column parameters are spelled `feature`, the
19
+ `RegexFeature` convention, since `name` names the pipeline step.
20
+ - `HistogramPlot.build` takes `stat` (default `"density"`), so count histograms no longer
21
+ need a raw seaborn call.
22
+
23
+ ### Fixed
24
+
25
+ - `ACFPlot` draws each lag as a vertical line (`ax.vlines`) instead of a 0.3-wide bar.
26
+ With hundreds of lags the bars were narrower than a pixel and aliased against the
27
+ pixel grid — lags vanished into phantom gaps or doubled up into dark stripes. Line
28
+ width is in points, so every lag now stays visible at any `nlags`.
29
+
30
+ ### Changed
31
+
32
+ - `BarPlot.build` no longer takes `xtick_rotation`; the per-call override
33
+ (`plot(..., xtick_rotation=45)`) and the `PlotSettings` field cover both paths.
34
+
35
+ ## 0.2.14
36
+
37
+ ### Added
38
+
39
+ - `PlotSettings.subplot_xlabels` / `subplot_ylabels`: per-panel axis labels, the
40
+ `subplot_titles` idea extended to labels. On the panels they cover they win over the
41
+ figure-wide `xlabel`/`ylabel`, so a multi-panel figure with panels showing different
42
+ quantities needs no `ax.set_ylabel` calls.
43
+ - `HorizontalLine`: a layer marking one value on the y-axis — the reference line a residual
44
+ plot is read against. Defaults to a quiet grey line rather than `VerticalLine`'s crimson
45
+ marker, because a baseline is context, not a finding.
46
+ - `BarPlot.build` takes `xtick_rotation` for category labels too long to sit flat.
47
+ - `PlotSettings.xtick_rotation`: the same rotation as a figure-wide setting, applied to
48
+ every axis — for date axes whose labels overlap when flat.
49
+
50
+ ### Changed
51
+
52
+ - Multi-panel grids from `create_figure` use constrained layout, which spaces panels,
53
+ labels and the suptitle at draw time — after every layer has landed — so figures no
54
+ longer need a trailing `fig.tight_layout()`.
55
+
56
+ ## 0.2.1
57
+
58
+ ### Breaking
59
+
60
+ - **`FileConfig` gained `date_column` and `date_index`.** `FileHandler.load` reads csv and
61
+ parquet (reader picked from the suffix), parses `date_column` as dates and, when
62
+ `date_index` is true, moves it into the index. The defaults (`"date"`, `True`) keep the
63
+ covid pipeline working unchanged, but a `FileConfig` subclass that overrode `load` to
64
+ support another shape can now express it as configuration instead. A file whose suffix has
65
+ no reader, or a `date_column` that is not in the frame, raises `ValueError`. Parquet needs
66
+ `pyarrow`, available as the `parquet` extra.
67
+ - **`DistributionRegistry` is a normal class.** Constructing one no longer returns a shared
68
+ process-wide instance, so a registration affects only the registry it was made on. Code
69
+ that registered a family on one registry and relied on a separately constructed
70
+ `DistributionFitter()` picking it up must now pass it: `DistributionFitter(registry)`.
71
+
72
+ ### Added
73
+
74
+ - `goad` console script: fits every family in the registry to one column of a csv or parquet
75
+ file and prints the fits ranked by log-likelihood (`goad data/residuals.csv residual`).
76
+ - `DistributionFitter` accepts an optional `DistributionRegistry`.
77
+ - `filehandler.reader_for`, `filehandler.write_frame` and `filehandler.parse_dates` as
78
+ reusable pieces; `FileHandler.save` writes parquet as well as csv.
79
+ - A pytest suite under `tests/`.
80
+
81
+ ### Fixed
82
+
83
+ - `BarWithDates.build` and `VerticalDate.build` return `(fig, ax)` like every other `build`.
84
+ - Plots draw on `self.ax` / `self.fig` instead of module-level `plt` calls, so a layer placed
85
+ with `plot_on_axes` lands on the axis it was handed rather than on matplotlib's current one.
86
+ - The `goad` console script points at a module that exists.
87
+
88
+ ### Changed
89
+
90
+ - `__version__` is read from the installed package metadata.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: goad-toolkit
3
- Version: 0.2.13
3
+ Version: 0.2.15
4
4
  Summary: An extensible toolkit for Goal Oriented Analysis of Data
5
5
  Project-URL: Github, https://github.com/raoulg/goad_toolkit
6
6
  Author-email: raoul grouls <Raoul.Grouls@han.nl>
@@ -60,6 +60,15 @@ input frame is safe, and the steps stay cheap.
60
60
  | `ZScaler` | standardise to mean 0, std 1 | `column`, `rename` |
61
61
  | `TimeFeatures` | derive calendar columns from a timestamp | `column`, `features` |
62
62
  | `RegexFeature` | count / flag / extract a pattern in a text column | `column`, `pattern`, `feature`, `mode` |
63
+ | `Filter` | keep the rows a `query` expression selects | `expr` |
64
+ | `SortValues` | sort rows by a column | `column`, `ascending` |
65
+ | `Head` | keep the first n rows — after a sort, a top-n | `n` |
66
+ | `CountValues` | one row per distinct value, with its count or share | `column`, `feature`, `normalize` |
67
+ | `GroupAgg` | one row per group, one aggregated value | `by`, `column`, `agg`, `feature` |
68
+ | `Share` | divide a column by its (per-group) total | `column`, `feature`, `by` |
69
+ | `FlagDates` | boolean column marking the listed dates | `column`, `dates`, `feature` |
70
+ | `MapValues` | map a column through a dict, with a default | `column`, `mapping`, `feature`, `default` |
71
+ | `SubtractBaseline` | subtract a baseline frame's column, keyed on a column | `column`, `baseline`, `on`, `feature` |
63
72
 
64
73
  `rename=True` writes to a new column (`deaths_shifted`, `deaths_zscore`, …) instead of
65
74
  overwriting. Prefer it. An overwritten column is a step you cannot debug, and the whole
@@ -51,6 +51,11 @@ class PlotSettings:
51
51
  grid: bool = False
52
52
  grid_alpha: Optional[float] = None
53
53
  subplot_titles: List[str] = field(default_factory=list)
54
+ subplot_xlabels: List[str] = field(default_factory=list)
55
+ subplot_ylabels: List[str] = field(default_factory=list)
56
+ xtick_rotation: Optional[float] = None
57
+ sharex: bool = False
58
+ sharey: bool = False
54
59
  ```
55
60
 
56
61
  `xlabel`/`ylabel` left as `None` are not blank — `create_figure` skips setting them at all,
@@ -60,13 +65,30 @@ column name whenever the label is still empty). Only an explicit `xlabel=`/`ylab
60
65
  correct axis labels for free from the column names you passed to `x=`/`y=`; reach for
61
66
  `xlabel=`/`ylabel=` only when the column name is not what you want a reader to see.
62
67
 
63
- It is a dataclass, so `dataclasses.replace(settings, title="Something else")` gives you a
64
- variant without mutating the original. Useful when a figure set shares everything but the
65
- title.
68
+ For a one-off change, override in the `plot` call itself: any keyword that names a
69
+ `PlotSettings` field is applied to that call and that call only, leaving the shared
70
+ settings object untouched.
71
+
72
+ ```python
73
+ BarPlot(settings).plot(data=west, x="quarter", y="revenue",
74
+ title="West region, on its own")
75
+ ```
76
+
77
+ That division of labour is deliberate: styling lives in settings, and the claim — the
78
+ title, the odd label — lives at the point of use. For a *lasting* variant, the dataclass
79
+ answer still applies: `dataclasses.replace(settings, figsize=(12, 4))` returns a new
80
+ settings object to keep and reuse.
66
81
 
67
82
  `subplot_titles` is what makes multi-panel figures work: when a plot creates more than one
68
83
  axis, each gets the corresponding entry. `PlotFits` uses this to label each fitted
69
- distribution with its own test statistics.
84
+ distribution with its own test statistics. `subplot_xlabels` and `subplot_ylabels` are the
85
+ same idea for axis labels — panels showing different quantities get their own labels from
86
+ the settings, not from `ax.set_ylabel` calls after the fact. On the panels they cover, the
87
+ lists win over the figure-wide `xlabel`/`ylabel`. `xtick_rotation` rotates the x tick
88
+ labels on every axis, for the date axes and long category names that overlap when flat.
89
+ `sharex`/`sharey` make a multi-panel grid share one scale — without them each panel
90
+ autoscales to its own data, and two shapes that look alike may sit on very different
91
+ axes.
70
92
 
71
93
  ## 3.3 `BasePlot`
72
94
 
@@ -89,6 +111,8 @@ The contract:
89
111
 
90
112
  `create_figure(n_plots=...)` handles the layout: one axis for a single plot, otherwise a grid
91
113
  wrapping at `settings.max_cols` columns — three by default — with the unused axes hidden.
114
+ The grid uses constrained layout, which spaces panels, labels and the suptitle at draw
115
+ time — after every layer has landed — so a figure never needs a `tight_layout()` call.
92
116
  Labels, grid and titles are applied to every visible axis. Set `max_cols=4` for a quartet or
93
117
  `max_cols=2` for a square grid; four panels at the default wrap to 3 + 1, which is rarely
94
118
  what a set of four wants.
@@ -132,7 +156,17 @@ changed is the difference between a chart and an argument. See
132
156
  [Visual critique](07-visual-critique.md) §"integrate text".
133
157
 
134
158
  `plot_on_axes(other_plot, ax, ...)` is the same thing targeting a specific axis, which is how
135
- multi-panel figures get built.
159
+ multi-panel figures get built. `FacetPlot` packages the most common multi-panel figure —
160
+ the same plot repeated once per level of a column — into a single call:
161
+
162
+ ```python
163
+ FacetPlot(settings).plot(inner=HistogramPlot(settings), data=msgs,
164
+ by="day_name", order=days, x="hour", bins=range(25))
165
+ ```
166
+
167
+ One panel per level of `by`, every panel drawn by `inner` with the same kwargs, panel
168
+ titles defaulting to the level names. Set `sharey=True` in the settings so the panels
169
+ share a scale and the shapes compare honestly.
136
170
 
137
171
  ## 3.5 Building up a plot, one layer at a time
138
172
 
@@ -158,6 +192,7 @@ step 3.
158
192
  | `ComparePlot` | two lines with labels | `LinePlot` twice via `plot_on` |
159
193
  | `VerticalDate` | a labelled vertical line at a date | a layer, not a plot |
160
194
  | `VerticalLine` | the same at a number | a layer, not a plot |
195
+ | `HorizontalLine` | a reference level on the y-axis, usually zero | a layer; the line a residual is read against |
161
196
  | `ComparePlotDate` | `ComparePlot` + `VerticalDate` | |
162
197
  | `BarPlot` | `sns.barplot`, one bar per category | see [Five families §4.1](04-five-families.md) |
163
198
  | `GroupedBarPlot` | bars split by `hue` | two categorical variables at once |
@@ -171,7 +206,8 @@ step 3.
171
206
  | `BarWithDates` | bars with a month locator on the x axis | |
172
207
  | `ResidualPlot` | `BarWithDates` + `VerticalDate` | the residual-over-time view |
173
208
  | `DecomposePlot` | `seasonal_decompose`, one panel each | observed/trend/seasonal/residual, overrides `plot` |
174
- | `ACFPlot` | `acf` as bars with a confidence band | see [Five families §4.2](04-five-families.md) |
209
+ | `FacetPlot` | any plot, once per level of a column | small multiples in one call; overrides `plot` |
210
+ | `ACFPlot` | `acf` as vertical lines with a confidence band | see [Five families §4.2](04-five-families.md) |
175
211
  | `HistogramPlot` | `sns.histplot`, `stat="density"` | density so a pdf can be overlaid |
176
212
  | `DistPlot` | a scipy distribution's pdf or pmf | estimates its own x-range from `ppf` |
177
213
  | `PlotFits` | histogram + fitted pdf, one panel per fit | see [Distributions](05-distributions.md) |
@@ -85,6 +85,15 @@ class TransformBase(ABC):
85
85
  | `ZScaler` | `column: str, rename: bool = False` |
86
86
  | `TimeFeatures` | `column: str, features: Optional[List[str]] = None` |
87
87
  | `RegexFeature` | `column: str, pattern: str, feature: str, mode: str = "count"` |
88
+ | `Filter` | `expr: str` — pandas `query` syntax |
89
+ | `SortValues` | `column: str, ascending: bool = True` |
90
+ | `Head` | `n: int` |
91
+ | `CountValues` | `column: str, feature: str = "n", normalize: bool = False` |
92
+ | `GroupAgg` | `by, column: Optional[str] = None, agg: str = "size", feature: Optional[str] = None` |
93
+ | `Share` | `column: str, feature: Optional[str] = None, by=None` |
94
+ | `FlagDates` | `column: str, dates, feature: str` |
95
+ | `MapValues` | `column: str, mapping: Dict, feature: str, default=None` |
96
+ | `SubtractBaseline` | `column: str, baseline: pd.DataFrame, on: str, feature: Optional[str] = None` |
88
97
 
89
98
  `rename=True` writes to `{column}_shifted` / `_diff` / `_rolling_avg` / `_zscore` instead of
90
99
  overwriting.
@@ -97,7 +106,14 @@ overwriting.
97
106
  capture group, NaN where nothing matched); anything else raises `ValueError`. `extract`
98
107
  requires exactly one capture group in `pattern` and logs its match rate through `loguru`.
99
108
  The output column is `feature`, not `name` — `Pipeline.add(name=...)` already claims `name`
100
- for the step.
109
+ for the step. The same spelling holds for every transform that names an output column
110
+ (`CountValues`, `GroupAgg`, `Share`, `FlagDates`, `MapValues`, `SubtractBaseline`).
111
+
112
+ `Filter`, `SortValues`, `Head`, `CountValues` and `GroupAgg` reshape the frame rather than
113
+ adding columns to it — like a bespoke parse step, they return a different frame, and the
114
+ steps after them see the new shape. `Share(by=...)` normalises within groups, so every
115
+ group sums to one separately. `SubtractBaseline` merges `baseline` on `on` and leaves the
116
+ difference — the residual of a model-and-residual move — flowing down the pipeline.
101
117
 
102
118
  ```python
103
119
  class Pipeline:
@@ -300,7 +316,12 @@ class PlotSettings:
300
316
  grid: bool = False
301
317
  grid_alpha: Optional[float] = None
302
318
  subplot_titles: List[str] = field(default_factory=list)
319
+ subplot_xlabels: List[str] = field(default_factory=list) # per-panel, wins over xlabel
320
+ subplot_ylabels: List[str] = field(default_factory=list) # per-panel, wins over ylabel
321
+ xtick_rotation: Optional[float] = None # rotate x tick labels, every axis
303
322
  max_cols: int = 3 # columns before wrapping
323
+ sharex: bool = False # subplot grids share one x-scale
324
+ sharey: bool = False # subplot grids share one y-scale
304
325
  highlight: List[str] = field(default_factory=list) # defaults for HighlightCategory
305
326
  base_color: str = "lightgrey"
306
327
  highlight_color: str = "crimson"
@@ -321,6 +342,10 @@ class BasePlot(ABC):
321
342
  def __init__(self, settings: PlotSettings, n_plots: Optional[int] = None)
322
343
  def plot(self, n_plots: Optional[int] = None, *args, **kwargs)
323
344
  def create_figure(self, n_plots: Optional[int] = None) # -> (fig, axes)
345
+ # plot() treats any kwarg naming a PlotSettings field (title, xlabel, ...) as a
346
+ # per-call override: applied to this instance before the figure is configured,
347
+ # the shared settings object untouched. On an existing figure (a layer), only
348
+ # the axis-level fields — title, xlabel, ylabel, xtick_rotation — still apply.
324
349
  def plot_on(self, other_plot: "BasePlot", *args, **kwargs)
325
350
  def plot_on_axes(self, other_plot: "BasePlot", ax: Axes, *args, **kwargs)
326
351
  def build(self, *args, **kwargs) # you implement
@@ -332,6 +357,7 @@ class BasePlot(ABC):
332
357
  | `ComparePlot` | `data, x, y1, y2, **kwargs` |
333
358
  | `VerticalDate` | `date: str, label: str` |
334
359
  | `VerticalLine` | `x: float, label=None, color="crimson", linestyle="--", linewidth=2, **kwargs` |
360
+ | `HorizontalLine` | `y: float, label=None, color="grey", linestyle="-", linewidth=1, **kwargs` |
335
361
  | `ComparePlotDate` | `data, x, y1, y2, date, datelabel, **kwargs` |
336
362
  | `BarPlot` | `data, x, y, order=None, **kwargs` → `sns.barplot` |
337
363
  | `GroupedBarPlot` | `data, x, y, hue, order=None, hue_order=None, **kwargs` |
@@ -346,9 +372,9 @@ class BasePlot(ABC):
346
372
  | `ScatterPlot` | `data, x, y, alpha=0.6, **kwargs` → `sns.scatterplot` (`hue`, `size`, `style`) |
347
373
  | `RegPlot` | `data, x, y, fit_reg=True, lowess=False, order=1, scatter=True, color="crimson", **kwargs` → `sns.regplot` |
348
374
  | `CorrelationHeatmap` | `data, columns=None, method="pearson", cmap="vlag", annot=True, fmt=".2f", **kwargs` |
349
- | `HistogramPlot` | `data: np.ndarray, bins=None, kde=False, color="skyblue", alpha=0.7, **kwargs` |
375
+ | `HistogramPlot` | `data, bins=None, kde=False, color="skyblue", alpha=0.7, stat="density", **kwargs` |
350
376
  | `DistPlot` | `distribution, x_range=None, samples=1000, color="crimson", linewidth=2, label=None, **kwargs` |
351
- | `ACFPlot` | `data, nlags=40, alpha=0.05, color="steelblue", band_color="gray", **kwargs` |
377
+ | `ACFPlot` | `data, nlags=40, alpha=0.05, color="steelblue", band_color="gray", linewidth=1.5, **kwargs` |
352
378
  | `QQPlot` | `data, distribution, color="steelblue", line_color="black", alpha=0.6, **kwargs` |
353
379
  | `ECDFPlot` | `data, compare=None, label="data", compare_label="compare", color="steelblue", compare_color="crimson", **kwargs` |
354
380
  | `NullPlot` | `result: NullResult, bins=None, alternative="two-sided", color="lightgrey", line_color="crimson", label=None, **kwargs` |
@@ -362,6 +388,10 @@ class PlotFits(BasePlot):
362
388
  class DecomposePlot(BasePlot):
363
389
  def plot(self, data: pd.DataFrame, column: str, period: int,
364
390
  model: str = "additive", **kwargs) -> Figure
391
+
392
+ class FacetPlot(BasePlot):
393
+ def plot(self, inner: BasePlot, data: pd.DataFrame, by: str,
394
+ order: Optional[List[str]] = None, **kwargs) # -> (fig, axes)
365
395
  ```
366
396
 
367
397
  Every `build` returns `(self.fig, self.ax)`, and every plot draws on `self.ax` / `self.fig`
@@ -369,8 +399,12 @@ rather than on matplotlib's current axes, so a layer placed with `plot_on_axes`
369
399
  axis it was handed.
370
400
 
371
401
  `PlotFits` overrides `plot` rather than `build`, because the panel count depends on the fit
372
- results. `HistogramPlot` uses `stat="density"` so a pdf overlays correctly; `DistPlot` takes a
373
- **frozen** distribution and falls back to `pmf` when there is no `pdf`.
402
+ results — and so does `FacetPlot`, whose panel count comes from the data: one panel per
403
+ level of `by`, each drawn by `inner` with the same kwargs, titles defaulting to the level
404
+ names. `HistogramPlot` defaults to `stat="density"` so a pdf overlays correctly (pass
405
+ `stat="count"` for plain counts); `DistPlot` takes a **frozen** distribution and falls back
406
+ to `pmf` when there is no `pdf`. `ACFPlot` draws each lag as a vertical line rather than a
407
+ bar — line width is in points, so hundreds of lags cannot alias into sub-pixel stripes.
374
408
 
375
409
  `NullPlot` composes `HistogramPlot` and `VerticalLine`: grey cloud, coloured line. It writes
376
410
  the observed value and its p-value into the legend, using `≤` when the p-value is at the
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "goad-toolkit"
3
- version = "0.2.13"
3
+ version = "0.2.15"
4
4
  description = "An extensible toolkit for Goal Oriented Analysis of Data"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -208,6 +208,178 @@ class RegexFeature(TransformBase):
208
208
  return data
209
209
 
210
210
 
211
+ class Filter(TransformBase):
212
+ """Keep the rows a query expression selects.
213
+
214
+ The expression is pandas' `DataFrame.query` syntax: column names bare,
215
+ `and`/`or`/`not` spelled out — e.g.
216
+ `"day_name == 'Thursday' and not is_release"`. A data decision written
217
+ as one readable line, which is what a pipeline step should be.
218
+ """
219
+
220
+ def transform(self, data: pd.DataFrame, expr: str) -> pd.DataFrame:
221
+ return data.query(expr)
222
+
223
+
224
+ class SortValues(TransformBase):
225
+ """Sort rows by a column. Order is a data decision, so it lives here."""
226
+
227
+ def transform(
228
+ self, data: pd.DataFrame, column: str, ascending: bool = True
229
+ ) -> pd.DataFrame:
230
+ return data.sort_values(column, ascending=ascending).reset_index(drop=True)
231
+
232
+
233
+ class Head(TransformBase):
234
+ """Keep the first `n` rows. After a sort, that is a top-n."""
235
+
236
+ def transform(self, data: pd.DataFrame, n: int) -> pd.DataFrame:
237
+ return data.head(n)
238
+
239
+
240
+ class CountValues(TransformBase):
241
+ """One row per distinct value of a column, with how often it occurs.
242
+
243
+ Sorted most-frequent first, the way `value_counts` sorts.
244
+ `normalize=True` turns the counts into shares of the total.
245
+ """
246
+
247
+ def transform(
248
+ self,
249
+ data: pd.DataFrame,
250
+ column: str,
251
+ feature: str = "n",
252
+ normalize: bool = False,
253
+ ) -> pd.DataFrame:
254
+ counts = data[column].value_counts(normalize=normalize)
255
+ return counts.rename(feature).rename_axis(column).reset_index()
256
+
257
+
258
+ class GroupAgg(TransformBase):
259
+ """Aggregate a column per group: one row per group, one value each.
260
+
261
+ `agg="size"` counts rows and needs no `column`; the count lands in
262
+ `feature` (default `"n"`). Any other `agg` is a pandas aggregation name
263
+ (`"sum"`, `"mean"`, `"nunique"`, ...) applied to `column`, landing in
264
+ `feature` (default: the column's own name). The output column is
265
+ `feature`, not `name` — `name` already names the pipeline step.
266
+ """
267
+
268
+ def transform(
269
+ self,
270
+ data: pd.DataFrame,
271
+ by,
272
+ column: Optional[str] = None,
273
+ agg: str = "size",
274
+ feature: Optional[str] = None,
275
+ ) -> pd.DataFrame:
276
+ if agg == "size":
277
+ result = data.groupby(by).size()
278
+ feature = feature or "n"
279
+ else:
280
+ if column is None:
281
+ raise ValueError(f"agg={agg!r} needs a column to aggregate")
282
+ result = data.groupby(by)[column].agg(agg)
283
+ feature = feature or column
284
+ return result.rename(feature).reset_index()
285
+
286
+
287
+ class Share(TransformBase):
288
+ """Divide a column by its total, so the values become shares summing to one.
289
+
290
+ With `by`, totals are per group instead: each row becomes its share of
291
+ its own group's total, and every group sums to one separately — the move
292
+ that makes a small group and a large one comparable by shape.
293
+ `feature` writes the shares to a new column; by default the column is
294
+ replaced in place.
295
+ """
296
+
297
+ def transform(
298
+ self,
299
+ data: pd.DataFrame,
300
+ column: str,
301
+ feature: Optional[str] = None,
302
+ by=None,
303
+ ) -> pd.DataFrame:
304
+ if by is None:
305
+ total = data[column].sum()
306
+ else:
307
+ total = data.groupby(by)[column].transform("sum")
308
+ data[feature or column] = data[column] / total
309
+ return data
310
+
311
+
312
+ class FlagDates(TransformBase):
313
+ """Mark the rows that fall on the given calendar days, as a boolean column.
314
+
315
+ The workhorse of event studies: flag release days, holidays, birthdays,
316
+ then filter or group on the flag. Matching is by calendar day — the
317
+ column's values are normalized to midnight, and timezone information is
318
+ dropped, before comparing — so a timestamp column works as-is. `dates`
319
+ is anything `pd.to_datetime` accepts.
320
+ """
321
+
322
+ def transform(
323
+ self, data: pd.DataFrame, column: str, dates, feature: str
324
+ ) -> pd.DataFrame:
325
+ values = pd.to_datetime(data[column])
326
+ if values.dt.tz is not None:
327
+ values = values.dt.tz_localize(None)
328
+ wanted = pd.to_datetime(list(dates))
329
+ if wanted.tz is not None:
330
+ wanted = wanted.tz_localize(None)
331
+ data[feature] = values.dt.normalize().isin(wanted.normalize())
332
+ return data
333
+
334
+
335
+ class MapValues(TransformBase):
336
+ """Write a new column by mapping an existing one through a dict.
337
+
338
+ Values missing from the mapping become `default`, so a two-entry
339
+ mapping plus a default covers "weekend, everything else weekday".
340
+ """
341
+
342
+ def transform(
343
+ self,
344
+ data: pd.DataFrame,
345
+ column: str,
346
+ mapping: Dict[Any, Any],
347
+ feature: str,
348
+ default: Optional[Any] = None,
349
+ ) -> pd.DataFrame:
350
+ data[feature] = data[column].map(mapping)
351
+ if default is not None:
352
+ data[feature] = data[feature].fillna(default)
353
+ return data
354
+
355
+
356
+ class SubtractBaseline(TransformBase):
357
+ """Subtract a baseline frame's column from the same column here, keyed on `on`.
358
+
359
+ The closing move of model-and-residual: the pipeline's frame carries the
360
+ measured values, `baseline` carries what the model says is normal, and
361
+ what flows on down the pipeline is the difference — the residual.
362
+ `feature` writes the difference to a new column; by default `column` is
363
+ replaced in place.
364
+ """
365
+
366
+ def transform(
367
+ self,
368
+ data: pd.DataFrame,
369
+ column: str,
370
+ baseline: pd.DataFrame,
371
+ on: str,
372
+ feature: Optional[str] = None,
373
+ ) -> pd.DataFrame:
374
+ colname = feature or column
375
+ merged = data.merge(
376
+ baseline[[on, column]], on=on, how="left", suffixes=("", "_baseline")
377
+ )
378
+ merged[colname] = merged[column] - merged[f"{column}_baseline"]
379
+ drop = [f"{column}_baseline"] + ([column] if colname != column else [])
380
+ return merged.drop(columns=drop)
381
+
382
+
211
383
  class Pipeline:
212
384
  """Pipeline for chaining data transformations."""
213
385