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.
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.remember/logs/memory-2026-08-31.log +33 -0
- goad_toolkit-0.2.15/.remember/tmp/last-save-ts +1 -0
- goad_toolkit-0.2.15/.remember/tmp/last-summary-failure +1 -0
- goad_toolkit-0.2.15/CHANGELOG.md +90 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/PKG-INFO +1 -1
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/docs/02-pipelines.md +9 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/docs/03-plot-composition.md +42 -6
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/docs/08-api-reference.md +39 -5
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/pyproject.toml +1 -1
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/src/goad_toolkit/datatransforms.py +172 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/src/goad_toolkit/visualizer.py +205 -17
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/tests/test_datatransforms.py +166 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/tests/test_visualizer.py +176 -1
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/uv.lock +1 -1
- goad_toolkit-0.2.13/.remember/tmp/last-save-ts +0 -1
- goad_toolkit-0.2.13/.remember/tmp/last-summary-failure +0 -1
- goad_toolkit-0.2.13/.remember/tmp/save-session.pid +0 -1
- goad_toolkit-0.2.13/CHANGELOG.md +0 -37
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/settings.local.json +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/.claude/settings.local.json +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/.gitignore +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/.python-version +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/CHANGELOG.md +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/MCP_SERVER.md +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/README.md +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/demo/linear.py +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/docs/01-goal-oriented-analysis.md +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/docs/02-pipelines.md +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/docs/03-plot-composition.md +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/docs/04-five-families.md +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/docs/05-distributions.md +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/docs/06-models-and-residuals.md +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/docs/07-visual-critique.md +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/docs/08-api-reference.md +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/docs/09-analysis-method.md +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/docs/10-teaching-path.md +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/docs/README.md +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/goad_mcp.py +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/img/distribution_fit.png +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/img/goaded.png +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/img/linear_results.png +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/img/null-distribution.png +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/img/residuals.png +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/img/zscores.png +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/pyproject.toml +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/src/goad_toolkit/__init__.py +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/src/goad_toolkit/analytics.py +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/src/goad_toolkit/cli.py +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/src/goad_toolkit/config.py +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/src/goad_toolkit/dataprocessor.py +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/src/goad_toolkit/datatransforms.py +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/src/goad_toolkit/distributions.py +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/src/goad_toolkit/filehandler.py +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/src/goad_toolkit/models.py +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/src/goad_toolkit/visualizer.py +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/tests/test_cli.py +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/tests/test_datatransforms.py +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/tests/test_distributions.py +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/tests/test_filehandler.py +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/tests/test_nulldistribution.py +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/tests/test_visualizer.py +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.claude/worktrees/quizzical-solomon-b62511/uv.lock +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.github/workflows/release.yml +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.gitignore +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.python-version +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.remember/.gitignore +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.remember/archive.md +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.remember/logs/autonomous/save-170051.log +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.remember/logs/autonomous/save-170257.log +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.remember/logs/autonomous/save-172752.log +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.remember/logs/autonomous/save-172954.log +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.remember/logs/autonomous/save-174001.log +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.remember/logs/autonomous/save-174204.log +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.remember/logs/autonomous/save-174415.log +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.remember/logs/autonomous/save-174621.log +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.remember/logs/autonomous/save-183124.log +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.remember/logs/autonomous/save-183325.log +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.remember/logs/autonomous/save-183803.log +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.remember/logs/autonomous/save-184713.log +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.remember/logs/autonomous/save-184913.log +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.remember/logs/hook-errors.log +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.remember/logs/logs-2026-08.tar.gz +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.remember/now.md +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.remember/recent.md +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.remember/tmp/capture-alive +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.remember/tmp/capture-alive.d/16b4788a-5507-4216-98ec-06bac13a9aba +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.remember/tmp/capture-alive.d/309ee39a-6171-47ea-88cf-ee1ad9433f68 +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.remember/tmp/capture-alive.d/90574e63-97cd-4c11-87e5-23b656c56fd9 +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.remember/tmp/case-divergence +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.remember/tmp/last-ndc.ts +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.remember/tmp/last-save.json +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.remember/tmp/post-tool-ran +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.remember/tmp/session-slug +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.remember/today-2026-08-10.done.md +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/.remember/today-2026-08-10.md +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/MCP_SERVER.md +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/README.md +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/demo/linear.py +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/docs/01-goal-oriented-analysis.md +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/docs/04-five-families.md +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/docs/05-distributions.md +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/docs/06-models-and-residuals.md +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/docs/07-visual-critique.md +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/docs/09-analysis-method.md +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/docs/10-teaching-path.md +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/docs/README.md +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/goad_mcp.py +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/img/distribution_fit.png +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/img/goaded.png +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/img/linear_results.png +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/img/null-distribution.png +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/img/residuals.png +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/img/zscores.png +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/src/goad_toolkit/__init__.py +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/src/goad_toolkit/analytics.py +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/src/goad_toolkit/cli.py +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/src/goad_toolkit/config.py +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/src/goad_toolkit/dataprocessor.py +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/src/goad_toolkit/distributions.py +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/src/goad_toolkit/filehandler.py +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/src/goad_toolkit/models.py +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/tests/test_cli.py +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/tests/test_distributions.py +0 -0
- {goad_toolkit-0.2.13 → goad_toolkit-0.2.15}/tests/test_filehandler.py +0 -0
- {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.
|
|
@@ -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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
| `
|
|
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
|
|
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
|
|
373
|
-
|
|
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
|
|
@@ -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
|
|