goad-toolkit 0.2.0__tar.gz → 0.2.2__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.2/.claude/settings.local.json +7 -0
- goad_toolkit-0.2.2/.claude/worktrees/quizzical-solomon-b62511/CHANGELOG.md +37 -0
- goad_toolkit-0.2.0/PKG-INFO → goad_toolkit-0.2.2/.claude/worktrees/quizzical-solomon-b62511/README.md +23 -21
- {goad_toolkit-0.2.0 → goad_toolkit-0.2.2/.claude/worktrees/quizzical-solomon-b62511}/docs/02-pipelines.md +10 -0
- {goad_toolkit-0.2.0 → goad_toolkit-0.2.2/.claude/worktrees/quizzical-solomon-b62511}/docs/03-plot-composition.md +16 -13
- {goad_toolkit-0.2.0 → goad_toolkit-0.2.2/.claude/worktrees/quizzical-solomon-b62511}/docs/05-distributions.md +11 -5
- {goad_toolkit-0.2.0 → goad_toolkit-0.2.2/.claude/worktrees/quizzical-solomon-b62511}/docs/08-api-reference.md +59 -17
- {goad_toolkit-0.2.0 → goad_toolkit-0.2.2/.claude/worktrees/quizzical-solomon-b62511}/docs/README.md +2 -1
- {goad_toolkit-0.2.0 → goad_toolkit-0.2.2/.claude/worktrees/quizzical-solomon-b62511}/pyproject.toml +12 -2
- goad_toolkit-0.2.2/.claude/worktrees/quizzical-solomon-b62511/src/goad_toolkit/__init__.py +6 -0
- {goad_toolkit-0.2.0 → goad_toolkit-0.2.2/.claude/worktrees/quizzical-solomon-b62511}/src/goad_toolkit/analytics.py +7 -3
- goad_toolkit-0.2.2/.claude/worktrees/quizzical-solomon-b62511/src/goad_toolkit/cli.py +92 -0
- {goad_toolkit-0.2.0 → goad_toolkit-0.2.2/.claude/worktrees/quizzical-solomon-b62511}/src/goad_toolkit/config.py +15 -1
- {goad_toolkit-0.2.0 → goad_toolkit-0.2.2/.claude/worktrees/quizzical-solomon-b62511}/src/goad_toolkit/distributions.py +27 -37
- goad_toolkit-0.2.2/.claude/worktrees/quizzical-solomon-b62511/src/goad_toolkit/filehandler.py +130 -0
- {goad_toolkit-0.2.0 → goad_toolkit-0.2.2/.claude/worktrees/quizzical-solomon-b62511}/src/goad_toolkit/visualizer.py +18 -10
- goad_toolkit-0.2.2/.claude/worktrees/quizzical-solomon-b62511/tests/test_cli.py +43 -0
- goad_toolkit-0.2.2/.claude/worktrees/quizzical-solomon-b62511/tests/test_distributions.py +72 -0
- goad_toolkit-0.2.2/.claude/worktrees/quizzical-solomon-b62511/tests/test_filehandler.py +101 -0
- goad_toolkit-0.2.2/.claude/worktrees/quizzical-solomon-b62511/tests/test_visualizer.py +113 -0
- {goad_toolkit-0.2.0 → goad_toolkit-0.2.2/.claude/worktrees/quizzical-solomon-b62511}/uv.lock +82 -1
- goad_toolkit-0.2.2/.gitignore +14 -0
- goad_toolkit-0.2.2/.python-version +1 -0
- goad_toolkit-0.2.2/.remember/.gitignore +1 -0
- goad_toolkit-0.2.2/.remember/logs/autonomous/save-195516.log +0 -0
- goad_toolkit-0.2.2/.remember/logs/autonomous/save-195720.log +0 -0
- goad_toolkit-0.2.2/.remember/logs/autonomous/save-195933.log +0 -0
- goad_toolkit-0.2.2/.remember/logs/autonomous/save-200831.log +0 -0
- goad_toolkit-0.2.2/.remember/logs/autonomous/save-201034.log +0 -0
- goad_toolkit-0.2.2/.remember/logs/hook-errors.log +0 -0
- goad_toolkit-0.2.2/.remember/logs/memory-2026-08-10.log +154 -0
- goad_toolkit-0.2.2/.remember/now.md +5 -0
- goad_toolkit-0.2.2/.remember/tmp/capture-alive +1 -0
- goad_toolkit-0.2.2/.remember/tmp/capture-alive.d/90574e63-97cd-4c11-87e5-23b656c56fd9 +0 -0
- goad_toolkit-0.2.2/.remember/tmp/case-divergence +2 -0
- goad_toolkit-0.2.2/.remember/tmp/last-ndc.ts +1 -0
- goad_toolkit-0.2.2/.remember/tmp/last-save-ts +1 -0
- goad_toolkit-0.2.2/.remember/tmp/last-save.json +1 -0
- goad_toolkit-0.2.2/.remember/tmp/now-day +1 -0
- goad_toolkit-0.2.2/.remember/tmp/post-tool-ran +0 -0
- goad_toolkit-0.2.2/.remember/tmp/save-session.pid +1 -0
- goad_toolkit-0.2.2/.remember/tmp/session-slug +7 -0
- goad_toolkit-0.2.2/.remember/today-2026-08-10.md +2 -0
- goad_toolkit-0.2.2/CHANGELOG.md +37 -0
- goad_toolkit-0.2.2/MCP_SERVER.md +205 -0
- goad_toolkit-0.2.2/PKG-INFO +268 -0
- {goad_toolkit-0.2.0 → goad_toolkit-0.2.2}/README.md +23 -3
- goad_toolkit-0.2.2/demo/linear.py +128 -0
- goad_toolkit-0.2.2/docs/01-goal-oriented-analysis.md +132 -0
- goad_toolkit-0.2.2/docs/02-pipelines.md +209 -0
- goad_toolkit-0.2.2/docs/03-plot-composition.md +225 -0
- goad_toolkit-0.2.2/docs/04-five-families.md +208 -0
- goad_toolkit-0.2.2/docs/05-distributions.md +230 -0
- goad_toolkit-0.2.2/docs/06-models-and-residuals.md +163 -0
- goad_toolkit-0.2.2/docs/07-visual-critique.md +130 -0
- goad_toolkit-0.2.2/docs/08-api-reference.md +321 -0
- goad_toolkit-0.2.2/docs/09-analysis-method.md +211 -0
- goad_toolkit-0.2.2/docs/10-teaching-path.md +172 -0
- goad_toolkit-0.2.2/docs/README.md +95 -0
- goad_toolkit-0.2.2/goad_mcp.py +1376 -0
- goad_toolkit-0.2.2/img/distribution_fit.png +0 -0
- goad_toolkit-0.2.2/img/goaded.png +0 -0
- goad_toolkit-0.2.2/img/linear_results.png +0 -0
- goad_toolkit-0.2.2/img/residuals.png +0 -0
- goad_toolkit-0.2.2/img/zscores.png +0 -0
- goad_toolkit-0.2.2/pyproject.toml +48 -0
- goad_toolkit-0.2.2/src/goad_toolkit/__init__.py +6 -0
- goad_toolkit-0.2.2/src/goad_toolkit/analytics.py +329 -0
- goad_toolkit-0.2.2/src/goad_toolkit/cli.py +92 -0
- goad_toolkit-0.2.2/src/goad_toolkit/config.py +38 -0
- goad_toolkit-0.2.2/src/goad_toolkit/dataprocessor.py +48 -0
- goad_toolkit-0.2.2/src/goad_toolkit/datatransforms.py +184 -0
- goad_toolkit-0.2.2/src/goad_toolkit/distributions.py +82 -0
- goad_toolkit-0.2.2/src/goad_toolkit/filehandler.py +130 -0
- goad_toolkit-0.2.2/src/goad_toolkit/models.py +44 -0
- goad_toolkit-0.2.2/src/goad_toolkit/visualizer.py +545 -0
- goad_toolkit-0.2.2/tests/test_cli.py +43 -0
- goad_toolkit-0.2.2/tests/test_distributions.py +72 -0
- goad_toolkit-0.2.2/tests/test_filehandler.py +101 -0
- goad_toolkit-0.2.2/tests/test_visualizer.py +113 -0
- goad_toolkit-0.2.2/uv.lock +2022 -0
- goad_toolkit-0.2.0/src/goad_toolkit/__init__.py +0 -1
- goad_toolkit-0.2.0/src/goad_toolkit/filehandler.py +0 -65
- {goad_toolkit-0.2.0 → goad_toolkit-0.2.2/.claude/worktrees/quizzical-solomon-b62511}/.gitignore +0 -0
- {goad_toolkit-0.2.0 → goad_toolkit-0.2.2/.claude/worktrees/quizzical-solomon-b62511}/.python-version +0 -0
- {goad_toolkit-0.2.0 → goad_toolkit-0.2.2/.claude/worktrees/quizzical-solomon-b62511}/MCP_SERVER.md +0 -0
- {goad_toolkit-0.2.0 → goad_toolkit-0.2.2/.claude/worktrees/quizzical-solomon-b62511}/demo/linear.py +0 -0
- {goad_toolkit-0.2.0 → goad_toolkit-0.2.2/.claude/worktrees/quizzical-solomon-b62511}/docs/01-goal-oriented-analysis.md +0 -0
- {goad_toolkit-0.2.0 → goad_toolkit-0.2.2/.claude/worktrees/quizzical-solomon-b62511}/docs/04-five-families.md +0 -0
- {goad_toolkit-0.2.0 → goad_toolkit-0.2.2/.claude/worktrees/quizzical-solomon-b62511}/docs/06-models-and-residuals.md +0 -0
- {goad_toolkit-0.2.0 → goad_toolkit-0.2.2/.claude/worktrees/quizzical-solomon-b62511}/docs/07-visual-critique.md +0 -0
- {goad_toolkit-0.2.0 → goad_toolkit-0.2.2/.claude/worktrees/quizzical-solomon-b62511}/docs/09-analysis-method.md +0 -0
- {goad_toolkit-0.2.0 → goad_toolkit-0.2.2/.claude/worktrees/quizzical-solomon-b62511}/docs/10-teaching-path.md +0 -0
- {goad_toolkit-0.2.0 → goad_toolkit-0.2.2/.claude/worktrees/quizzical-solomon-b62511}/goad_mcp.py +0 -0
- {goad_toolkit-0.2.0 → goad_toolkit-0.2.2/.claude/worktrees/quizzical-solomon-b62511}/img/distribution_fit.png +0 -0
- {goad_toolkit-0.2.0 → goad_toolkit-0.2.2/.claude/worktrees/quizzical-solomon-b62511}/img/goaded.png +0 -0
- {goad_toolkit-0.2.0 → goad_toolkit-0.2.2/.claude/worktrees/quizzical-solomon-b62511}/img/linear_results.png +0 -0
- {goad_toolkit-0.2.0 → goad_toolkit-0.2.2/.claude/worktrees/quizzical-solomon-b62511}/img/residuals.png +0 -0
- {goad_toolkit-0.2.0 → goad_toolkit-0.2.2/.claude/worktrees/quizzical-solomon-b62511}/img/zscores.png +0 -0
- {goad_toolkit-0.2.0 → goad_toolkit-0.2.2/.claude/worktrees/quizzical-solomon-b62511}/src/goad_toolkit/dataprocessor.py +0 -0
- {goad_toolkit-0.2.0 → goad_toolkit-0.2.2/.claude/worktrees/quizzical-solomon-b62511}/src/goad_toolkit/datatransforms.py +0 -0
- {goad_toolkit-0.2.0 → goad_toolkit-0.2.2/.claude/worktrees/quizzical-solomon-b62511}/src/goad_toolkit/models.py +0 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.2.1
|
|
4
|
+
|
|
5
|
+
### Breaking
|
|
6
|
+
|
|
7
|
+
- **`FileConfig` gained `date_column` and `date_index`.** `FileHandler.load` reads csv and
|
|
8
|
+
parquet (reader picked from the suffix), parses `date_column` as dates and, when
|
|
9
|
+
`date_index` is true, moves it into the index. The defaults (`"date"`, `True`) keep the
|
|
10
|
+
covid pipeline working unchanged, but a `FileConfig` subclass that overrode `load` to
|
|
11
|
+
support another shape can now express it as configuration instead. A file whose suffix has
|
|
12
|
+
no reader, or a `date_column` that is not in the frame, raises `ValueError`. Parquet needs
|
|
13
|
+
`pyarrow`, available as the `parquet` extra.
|
|
14
|
+
- **`DistributionRegistry` is a normal class.** Constructing one no longer returns a shared
|
|
15
|
+
process-wide instance, so a registration affects only the registry it was made on. Code
|
|
16
|
+
that registered a family on one registry and relied on a separately constructed
|
|
17
|
+
`DistributionFitter()` picking it up must now pass it: `DistributionFitter(registry)`.
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- `goad` console script: fits every family in the registry to one column of a csv or parquet
|
|
22
|
+
file and prints the fits ranked by log-likelihood (`goad data/residuals.csv residual`).
|
|
23
|
+
- `DistributionFitter` accepts an optional `DistributionRegistry`.
|
|
24
|
+
- `filehandler.reader_for`, `filehandler.write_frame` and `filehandler.parse_dates` as
|
|
25
|
+
reusable pieces; `FileHandler.save` writes parquet as well as csv.
|
|
26
|
+
- A pytest suite under `tests/`.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- `BarWithDates.build` and `VerticalDate.build` return `(fig, ax)` like every other `build`.
|
|
31
|
+
- Plots draw on `self.ax` / `self.fig` instead of module-level `plt` calls, so a layer placed
|
|
32
|
+
with `plot_on_axes` lands on the axis it was handed rather than on matplotlib's current one.
|
|
33
|
+
- The `goad` console script points at a module that exists.
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
- `__version__` is read from the installed package metadata.
|
|
@@ -1,21 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: goad-toolkit
|
|
3
|
-
Version: 0.2.0
|
|
4
|
-
Summary: An extensible toolkit for Goal Oriented Analysis of Data
|
|
5
|
-
Project-URL: Github, https://github.com/raoulg/goad_toolkit
|
|
6
|
-
Author-email: raoul grouls <Raoul.Grouls@han.nl>
|
|
7
|
-
Requires-Python: >=3.12
|
|
8
|
-
Requires-Dist: loguru>=0.7.3
|
|
9
|
-
Requires-Dist: matplotlib>=3.10.1
|
|
10
|
-
Requires-Dist: numpy>=2.2.4
|
|
11
|
-
Requires-Dist: pandas>=2.2.3
|
|
12
|
-
Requires-Dist: pydantic>=2.10.6
|
|
13
|
-
Requires-Dist: requests>=2.32.3
|
|
14
|
-
Requires-Dist: scipy>=1.15.2
|
|
15
|
-
Requires-Dist: seaborn>=0.13.2
|
|
16
|
-
Requires-Dist: tqdm>=4.67.1
|
|
17
|
-
Description-Content-Type: text/markdown
|
|
18
|
-
|
|
19
1
|
# GOAD🐐 is the GOAT - Goal Oriented Analysis of Data
|
|
20
2
|
[](https://github.com/astral-sh/uv)
|
|
21
3
|
[](https://pypi.org/project/goad_toolkit/)
|
|
@@ -61,6 +43,23 @@ Or, if you prefer your dependencies to be installed 100x slower, with pip:
|
|
|
61
43
|
pip install goad-toolkit
|
|
62
44
|
```
|
|
63
45
|
|
|
46
|
+
Add the `parquet` extra if you want to read and write parquet files:
|
|
47
|
+
```bash
|
|
48
|
+
uv add "goad-toolkit[parquet]"
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Command line
|
|
52
|
+
|
|
53
|
+
The install ships a `goad` command that fits every distribution in the registry to one column
|
|
54
|
+
of a csv or parquet file and prints the fits, ranked by log-likelihood:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
goad data/residuals.csv residual
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Use `--discrete` for count data, `--criterion {likelihood,ks,combined}` to pick which fits are
|
|
61
|
+
marked best, and `goad --help` for the rest.
|
|
62
|
+
|
|
64
63
|
## 📋 Demo: Linear Model Analysis
|
|
65
64
|
|
|
66
65
|
GOAD🐐 includes a comprehensive [demo](demo/linear.py) that shows how to use its components together.
|
|
@@ -147,7 +146,7 @@ class ComparePlot(BasePlot):
|
|
|
147
146
|
compare = LinePlot(self.settings)
|
|
148
147
|
self.plot_on(compare, data=data, x=x, y=y1, label=y1, **kwargs)
|
|
149
148
|
self.plot_on(compare, data=data, x=x, y=y2, label=y2, **kwargs)
|
|
150
|
-
|
|
149
|
+
self.ax.tick_params(axis="x", rotation=45)
|
|
151
150
|
|
|
152
151
|
return self.fig, self.ax
|
|
153
152
|
|
|
@@ -203,12 +202,15 @@ registry.register_distribution(
|
|
|
203
202
|
num_params=2
|
|
204
203
|
)
|
|
205
204
|
|
|
206
|
-
#
|
|
205
|
+
# Hand the registry to the fitter to use your families
|
|
207
206
|
from goad_toolkit.analytics import DistributionFitter
|
|
208
|
-
fitter = DistributionFitter()
|
|
207
|
+
fitter = DistributionFitter(registry)
|
|
209
208
|
print(fitter.registry) # shows all registered distributions
|
|
210
209
|
```
|
|
211
210
|
|
|
211
|
+
Registrations belong to the registry you made them on, so a fitter built without one
|
|
212
|
+
(`DistributionFitter()`) uses a fresh registry with only the shipped families.
|
|
213
|
+
|
|
212
214
|
|
|
213
215
|
|
|
214
216
|
## 🔧 Advanced Usage: Composing Plots
|
|
@@ -175,6 +175,16 @@ data = processor.process(save=True) # and writes the result to data/processed/
|
|
|
175
175
|
Subclasses implement exactly one method: `config_pipeline`. Everything else — the download,
|
|
176
176
|
the directory creation, the raw/processed split, the save — is inherited.
|
|
177
177
|
|
|
178
|
+
`FileConfig` also says how the file is read: the reader follows the suffix (`.csv` or
|
|
179
|
+
`.parquet`), `date_column` names the column to parse as dates, and `date_index` decides
|
|
180
|
+
whether it becomes the index. For a dataset without a time axis, set `date_column=None`.
|
|
181
|
+
|
|
182
|
+
```python
|
|
183
|
+
config = FileConfig(
|
|
184
|
+
data_dir=Path("data"), filename=Path("sales.parquet"), date_column="ordered_at"
|
|
185
|
+
)
|
|
186
|
+
```
|
|
187
|
+
|
|
178
188
|
The pipeline's parameters come from a pydantic `DataConfig`, which means the tuning knobs of
|
|
179
189
|
your analysis (the shift period, the smoothing window, the date range) live in one typed
|
|
180
190
|
object rather than scattered as literals through the steps. When somebody asks "what happens
|
|
@@ -98,7 +98,7 @@ class ComparePlot(BasePlot):
|
|
|
98
98
|
line = LinePlot(self.settings)
|
|
99
99
|
self.plot_on(line, data=data, x=x, y=y1, label=y1, **kwargs)
|
|
100
100
|
self.plot_on(line, data=data, x=x, y=y2, label=y2, **kwargs)
|
|
101
|
-
|
|
101
|
+
self.ax.tick_params(axis="x", rotation=45)
|
|
102
102
|
return self.fig, self.ax
|
|
103
103
|
```
|
|
104
104
|
|
|
@@ -113,7 +113,7 @@ class ResidualPlot(BasePlot):
|
|
|
113
113
|
def build(self, data, x, y, date, datelabel, interval=1):
|
|
114
114
|
self.plot_on(BarWithDates(self.settings), data=data, x=x, y=y, interval=interval)
|
|
115
115
|
self.plot_on(VerticalDate(self.settings), date=date, label=datelabel)
|
|
116
|
-
|
|
116
|
+
self.ax.tick_params(axis="x", rotation=45)
|
|
117
117
|
return self.fig, self.ax
|
|
118
118
|
```
|
|
119
119
|
|
|
@@ -205,17 +205,20 @@ scatter.plot_on(TrendLine(settings), data=df, x="weight", y="mpg", order=2)
|
|
|
205
205
|
Grey data, one coloured layer carrying the message. That is guideline five, expressed as two
|
|
206
206
|
lines of code.
|
|
207
207
|
|
|
208
|
-
## 3.8
|
|
209
|
-
|
|
210
|
-
- `
|
|
211
|
-
|
|
212
|
-
- `
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
208
|
+
## 3.8 Two rules that keep compositions composable
|
|
209
|
+
|
|
210
|
+
- **`build` returns `(self.fig, self.ax)`.** Every shipped plot does, layers included, so a
|
|
211
|
+
caller can keep working with the figure whichever piece it got back. Yours should too.
|
|
212
|
+
- **Draw on `self.ax` and `self.fig`, never on `plt`.** Module-level calls like
|
|
213
|
+
`plt.xticks(...)` or `plt.axvline(...)` act on matplotlib's *current* axes, which is not
|
|
214
|
+
necessarily the axis your plot was handed — in a multi-panel figure it is usually the wrong
|
|
215
|
+
one. The explicit forms are `self.ax.tick_params(axis="x", rotation=45)`,
|
|
216
|
+
`self.ax.axvline(...)`, `self.fig.suptitle(...)`. Keeping to them is what makes
|
|
217
|
+
`plot_on_axes` reliable.
|
|
218
|
+
|
|
219
|
+
`PlotFits` overrides `plot` rather than `build`, because it needs to know how many panels to
|
|
220
|
+
create before the figure exists. Its `build` is a no-op. It is the exception, not a pattern to
|
|
221
|
+
copy.
|
|
219
222
|
|
|
220
223
|
---
|
|
221
224
|
|
|
@@ -45,11 +45,17 @@ Two things to know before you rely on it:
|
|
|
45
45
|
|
|
46
46
|
- **`num_params` must be right.** It selects the bounds template the fitter builds (see §5.3),
|
|
47
47
|
so a wrong count produces a confusing optimisation failure rather than a clear error.
|
|
48
|
-
- **
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
48
|
+
- **Registrations are scoped to the registry object.** Every `DistributionRegistry()` starts
|
|
49
|
+
from the shipped families, so a registration in one cell affects only the fits that use that
|
|
50
|
+
registry, and a fresh instance is your clean slate — no kernel restart needed. Pass the
|
|
51
|
+
registry you extended to the fitter:
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
fitter = DistributionFitter(registry)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`DistributionFitter()` without an argument builds its own registry with the shipped
|
|
58
|
+
families.
|
|
53
59
|
|
|
54
60
|
## 5.3 `DistributionFitter`
|
|
55
61
|
|
|
@@ -14,6 +14,8 @@ class FileConfig(BaseModel):
|
|
|
14
14
|
data_dir: Path = Path.home() / ".cache/mads_datasets/covid"
|
|
15
15
|
filename: Path = Path("covid.csv")
|
|
16
16
|
url: str = "https://raw.githubusercontent.com/mzelst/covid-19/master/data/rivm_by_day.csv"
|
|
17
|
+
date_column: Optional[str] = "date"
|
|
18
|
+
date_index: bool = True
|
|
17
19
|
|
|
18
20
|
class DataConfig(BaseModel):
|
|
19
21
|
period: int = -14
|
|
@@ -22,6 +24,9 @@ class DataConfig(BaseModel):
|
|
|
22
24
|
end_date: str = "2021-06-01"
|
|
23
25
|
```
|
|
24
26
|
|
|
27
|
+
`date_column` names the column parsed as dates; `None` for data without a time axis.
|
|
28
|
+
`date_index` decides whether that column becomes the index.
|
|
29
|
+
|
|
25
30
|
`DataConfig` holds the tuning knobs a pipeline reads. Subclass it for your own analysis
|
|
26
31
|
rather than passing loose numbers into `Pipeline.add`.
|
|
27
32
|
|
|
@@ -30,6 +35,13 @@ rather than passing loose numbers into `Pipeline.add`.
|
|
|
30
35
|
## 8.2 `goad_toolkit.filehandler`
|
|
31
36
|
|
|
32
37
|
```python
|
|
38
|
+
READERS: dict[str, Callable[[Path], pd.DataFrame]] # ".csv", ".parquet"
|
|
39
|
+
|
|
40
|
+
def reader_for(path: Path) -> Callable[[Path], pd.DataFrame]
|
|
41
|
+
def write_frame(data: pd.DataFrame, path: Path) -> None
|
|
42
|
+
def parse_dates(data: pd.DataFrame, date_column: Optional[str],
|
|
43
|
+
as_index: bool) -> pd.DataFrame
|
|
44
|
+
|
|
33
45
|
class FileHandler:
|
|
34
46
|
def __init__(self, config: FileConfig)
|
|
35
47
|
def download(self, filename: Optional[Path] = None) -> None
|
|
@@ -41,8 +53,13 @@ class FileHandler:
|
|
|
41
53
|
- Creates `data_dir`, `data_dir/raw` and `data_dir/processed` on construction.
|
|
42
54
|
- `load(raw=True)` reads from `raw/` and downloads first if the file is missing;
|
|
43
55
|
`load(raw=False)` reads from `processed/`.
|
|
44
|
-
-
|
|
45
|
-
|
|
56
|
+
- The reader and writer follow the file suffix: `.csv` and `.parquet`. Anything else raises a
|
|
57
|
+
`ValueError` naming the supported suffixes. Parquet needs `pyarrow`, installed with the
|
|
58
|
+
`parquet` extra: `uv add "goad-toolkit[parquet]"`.
|
|
59
|
+
- Date handling follows `FileConfig`: `date_column` is parsed as dates and, when
|
|
60
|
+
`date_index` is true, moved into the index. A frame whose dates already *are* the index is
|
|
61
|
+
accepted too, so a `save` → `load` round trip keeps its shape. A missing `date_column`
|
|
62
|
+
raises a `ValueError` listing the columns that are there.
|
|
46
63
|
|
|
47
64
|
---
|
|
48
65
|
|
|
@@ -109,7 +126,7 @@ class Distribution:
|
|
|
109
126
|
is_discrete: bool
|
|
110
127
|
num_params: int
|
|
111
128
|
|
|
112
|
-
class DistributionRegistry:
|
|
129
|
+
class DistributionRegistry:
|
|
113
130
|
def register_distribution(self, name: str, dist, is_discrete: bool, num_params: int)
|
|
114
131
|
def get_distribution(self, name: str) -> Distribution
|
|
115
132
|
def get_names(self) -> List[str]
|
|
@@ -117,7 +134,8 @@ class DistributionRegistry: # process-wide singleton
|
|
|
117
134
|
```
|
|
118
135
|
|
|
119
136
|
Registered by default: `norm`, `uniform`, `lognorm`, `poisson`, `exponential`, `skewnorm`,
|
|
120
|
-
`gamma`, `weibull`.
|
|
137
|
+
`gamma`, `weibull`. Every instance starts from that set, so a registration is scoped to the
|
|
138
|
+
registry you made it on — hand that registry to `DistributionFitter(registry)` to use it.
|
|
121
139
|
|
|
122
140
|
---
|
|
123
141
|
|
|
@@ -156,7 +174,9 @@ Result = Union[FitResult, FailedFit]
|
|
|
156
174
|
|
|
157
175
|
```python
|
|
158
176
|
class DistributionFitter:
|
|
159
|
-
registry:
|
|
177
|
+
def __init__(self, registry: Optional[DistributionRegistry] = None)
|
|
178
|
+
|
|
179
|
+
registry: list[str] # property, the names
|
|
160
180
|
|
|
161
181
|
def fit(self, data: np.ndarray, discrete: bool,
|
|
162
182
|
method: str = "mle",
|
|
@@ -255,24 +275,46 @@ class PlotFits(BasePlot):
|
|
|
255
275
|
fitplotsettings: FitPlotSettings) -> Figure
|
|
256
276
|
```
|
|
257
277
|
|
|
278
|
+
Every `build` returns `(self.fig, self.ax)`, and every plot draws on `self.ax` / `self.fig`
|
|
279
|
+
rather than on matplotlib's current axes, so a layer placed with `plot_on_axes` lands on the
|
|
280
|
+
axis it was handed.
|
|
281
|
+
|
|
258
282
|
`PlotFits` overrides `plot` rather than `build`, because the panel count depends on the fit
|
|
259
283
|
results. `HistogramPlot` uses `stat="density"` so a pdf overlays correctly; `DistPlot` takes a
|
|
260
284
|
**frozen** distribution and falls back to `pmf` when there is no `pdf`.
|
|
261
285
|
|
|
262
286
|
---
|
|
263
287
|
|
|
264
|
-
## 8.9
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
288
|
+
## 8.9 `goad_toolkit.cli`
|
|
289
|
+
|
|
290
|
+
The `goad` console script fits every family in the registry to one column and prints the fits
|
|
291
|
+
ranked by log-likelihood.
|
|
292
|
+
|
|
293
|
+
```bash
|
|
294
|
+
goad data/residuals.parquet residual
|
|
295
|
+
goad data/counts.csv events --discrete --criterion ks
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
```python
|
|
299
|
+
def parse_args(argv: List[str] | None = None) -> argparse.Namespace
|
|
300
|
+
def load_column(file: Path, column: str) -> np.ndarray # dropna, as an array
|
|
301
|
+
def format_results(results: List[Result]) -> str
|
|
302
|
+
def main(argv: List[str] | None = None) -> None
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
`--discrete` fits the discrete families instead of the continuous ones; `--criterion` is one
|
|
306
|
+
of `likelihood`, `ks`, `combined`; `--method` is passed to `scipy.stats.fit`.
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
## 8.10 Rough edges worth knowing
|
|
311
|
+
|
|
312
|
+
- `PlotFits` overrides `plot` rather than `build`; its `build` is a no-op. It is the one
|
|
313
|
+
exception to the `build` contract.
|
|
314
|
+
- Fits that fail come back as `FailedFit` values in the result list rather than raising, so
|
|
315
|
+
a list of results can contain both kinds and needs an `isinstance` check.
|
|
316
|
+
- `Pipeline.apply` copies the frame once and then mutates that copy in place, so transforms
|
|
317
|
+
see each other's columns — the order you add steps in matters.
|
|
276
318
|
|
|
277
319
|
---
|
|
278
320
|
|
{goad_toolkit-0.2.0 → goad_toolkit-0.2.2/.claude/worktrees/quizzical-solomon-b62511}/docs/README.md
RENAMED
|
@@ -83,7 +83,8 @@ src/goad_toolkit/
|
|
|
83
83
|
├── distributions.py # Distribution, DistributionRegistry
|
|
84
84
|
├── analytics.py # DistributionFitter, FitResult, FailedFit, KSTestResult
|
|
85
85
|
├── models.py # linear_model, logistic, mse, train_model
|
|
86
|
-
|
|
86
|
+
├── visualizer.py # PlotSettings + BasePlot and its subclasses, PlotFits
|
|
87
|
+
└── cli.py # the `goad` command: fit the registry to one column
|
|
87
88
|
```
|
|
88
89
|
|
|
89
90
|
Plus, outside the package:
|
{goad_toolkit-0.2.0 → goad_toolkit-0.2.2/.claude/worktrees/quizzical-solomon-b62511}/pyproject.toml
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "goad-toolkit"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.1"
|
|
4
4
|
description = "An extensible toolkit for Goal Oriented Analysis of Data"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
@@ -20,8 +20,11 @@ dependencies = [
|
|
|
20
20
|
]
|
|
21
21
|
urls = {Github = "https://github.com/raoulg/goad_toolkit"}
|
|
22
22
|
|
|
23
|
+
[project.optional-dependencies]
|
|
24
|
+
parquet = ["pyarrow>=19.0.0"]
|
|
25
|
+
|
|
23
26
|
[project.scripts]
|
|
24
|
-
goad = "
|
|
27
|
+
goad = "goad_toolkit.cli:main"
|
|
25
28
|
|
|
26
29
|
[build-system]
|
|
27
30
|
requires = ["hatchling"]
|
|
@@ -31,8 +34,15 @@ build-backend = "hatchling.build"
|
|
|
31
34
|
dev = [
|
|
32
35
|
"isort>=6.0.1",
|
|
33
36
|
"jupyter>=1.1.1",
|
|
37
|
+
"pyarrow>=19.0.0",
|
|
34
38
|
"pyright>=1.1.397",
|
|
39
|
+
"pytest>=8.3.5",
|
|
35
40
|
"ruff>=0.11.0",
|
|
36
41
|
]
|
|
37
42
|
[tool.ruff.lint]
|
|
38
43
|
select = ["E4", "E7", "E9", "F", "N", "C4", "DTZ", "PT", "S"]
|
|
44
|
+
|
|
45
|
+
[tool.ruff.lint.per-file-ignores]
|
|
46
|
+
# assert is the assertion mechanism in tests and the demo script
|
|
47
|
+
"tests/**" = ["S101"]
|
|
48
|
+
"demo/**" = ["S101"]
|
|
@@ -75,9 +75,13 @@ Result = Union[FitResult, FailedFit]
|
|
|
75
75
|
class DistributionFitter:
|
|
76
76
|
"""Class to fit distributions from a registry to data."""
|
|
77
77
|
|
|
78
|
-
def __init__(self) -> None:
|
|
79
|
-
"""Initialize with a distribution registry.
|
|
80
|
-
|
|
78
|
+
def __init__(self, registry: Optional[DistributionRegistry] = None) -> None:
|
|
79
|
+
"""Initialize with a distribution registry.
|
|
80
|
+
|
|
81
|
+
Pass a registry to fit a custom set of families; a fresh registry with
|
|
82
|
+
the shipped families is used otherwise.
|
|
83
|
+
"""
|
|
84
|
+
self._registry = registry if registry is not None else DistributionRegistry()
|
|
81
85
|
|
|
82
86
|
@property
|
|
83
87
|
def registry(self) -> list[str]:
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Command line entry point: fit the distribution registry to one column."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import List
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from goad_toolkit.analytics import DistributionFitter, FitResult, Result
|
|
10
|
+
from goad_toolkit.filehandler import reader_for
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def parse_args(argv: List[str] | None = None) -> argparse.Namespace:
|
|
14
|
+
parser = argparse.ArgumentParser(
|
|
15
|
+
prog="goad",
|
|
16
|
+
description=(
|
|
17
|
+
"Fit every distribution in the registry to one column of a csv or "
|
|
18
|
+
"parquet file and print the fits, ranked by log-likelihood."
|
|
19
|
+
),
|
|
20
|
+
)
|
|
21
|
+
parser.add_argument("file", type=Path, help="path to a .csv or .parquet file")
|
|
22
|
+
parser.add_argument("column", help="name of the column to fit")
|
|
23
|
+
parser.add_argument(
|
|
24
|
+
"--discrete",
|
|
25
|
+
action="store_true",
|
|
26
|
+
help="fit the discrete families instead of the continuous ones",
|
|
27
|
+
)
|
|
28
|
+
parser.add_argument(
|
|
29
|
+
"--criterion",
|
|
30
|
+
default="combined",
|
|
31
|
+
choices=["likelihood", "ks", "combined"],
|
|
32
|
+
help="which fits to mark as best (default: combined)",
|
|
33
|
+
)
|
|
34
|
+
parser.add_argument(
|
|
35
|
+
"--method", default="mle", help="scipy fitting method (default: mle)"
|
|
36
|
+
)
|
|
37
|
+
return parser.parse_args(argv)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def load_column(file: Path, column: str) -> np.ndarray:
|
|
41
|
+
"""Read one column and return it as a float array without missing values."""
|
|
42
|
+
if not file.exists():
|
|
43
|
+
raise SystemExit(f"No such file: {file}")
|
|
44
|
+
data = reader_for(file)(file)
|
|
45
|
+
if column not in data.columns:
|
|
46
|
+
raise SystemExit(
|
|
47
|
+
f"Column '{column}' not found. Available: {list(data.columns)}"
|
|
48
|
+
)
|
|
49
|
+
return data[column].dropna().to_numpy()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def format_results(results: List[Result]) -> str:
|
|
53
|
+
"""Render fits as a table, best log-likelihood first, failures last."""
|
|
54
|
+
successes = [fit for fit in results if isinstance(fit, FitResult)]
|
|
55
|
+
failures = [fit for fit in results if not isinstance(fit, FitResult)]
|
|
56
|
+
successes.sort(
|
|
57
|
+
key=lambda fit: fit.log_likelihood
|
|
58
|
+
if fit.log_likelihood is not None
|
|
59
|
+
else -np.inf,
|
|
60
|
+
reverse=True,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
header = f"{'distribution':<14}{'loglik':>14}{'ks p':>10} best"
|
|
64
|
+
lines = [header, "-" * len(header)]
|
|
65
|
+
for fit in successes:
|
|
66
|
+
loglik = "n/a" if fit.log_likelihood is None else f"{fit.log_likelihood:.2f}"
|
|
67
|
+
ks = "n/a" if fit.kstest is None else f"{fit.kstest.p_value:.4f}"
|
|
68
|
+
marks = []
|
|
69
|
+
if fit.best_likelihood:
|
|
70
|
+
marks.append("likelihood")
|
|
71
|
+
if fit.best_ks:
|
|
72
|
+
marks.append("ks")
|
|
73
|
+
row = f"{fit.distribution:<14}{loglik:>14}{ks:>10} {', '.join(marks)}"
|
|
74
|
+
lines.append(row.rstrip())
|
|
75
|
+
for fit in failures:
|
|
76
|
+
lines.append(f"{fit.distribution:<14}{'failed: ' + fit.message}")
|
|
77
|
+
return "\n".join(lines)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def main(argv: List[str] | None = None) -> None:
|
|
81
|
+
args = parse_args(argv)
|
|
82
|
+
values = load_column(args.file, args.column)
|
|
83
|
+
fitter = DistributionFitter()
|
|
84
|
+
results = fitter.fit(
|
|
85
|
+
values, discrete=args.discrete, method=args.method, criterion=args.criterion
|
|
86
|
+
)
|
|
87
|
+
print(f"\n{len(values)} values from {args.file}:{args.column}\n")
|
|
88
|
+
print(format_results(results))
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
if __name__ == "__main__":
|
|
92
|
+
main()
|
|
@@ -1,17 +1,31 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
|
+
from typing import Optional
|
|
2
3
|
|
|
3
4
|
from pydantic import BaseModel
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
class FileConfig(BaseModel):
|
|
8
|
+
"""Where the data lives and how to read it.
|
|
9
|
+
|
|
10
|
+
`date_column` names the column to parse as dates; set it to None for data
|
|
11
|
+
without a time axis. `date_index` decides whether that column becomes the
|
|
12
|
+
index. The reader is picked from the file suffix (`.csv`, `.parquet`).
|
|
13
|
+
"""
|
|
14
|
+
|
|
7
15
|
data_dir: Path = Path.home() / ".cache/mads_datasets/covid"
|
|
8
16
|
filename: Path = Path("covid.csv")
|
|
9
17
|
url: str = (
|
|
10
18
|
"https://raw.githubusercontent.com/mzelst/covid-19/master/data/rivm_by_day.csv"
|
|
11
19
|
)
|
|
20
|
+
date_column: Optional[str] = "date"
|
|
21
|
+
date_index: bool = True
|
|
12
22
|
|
|
13
23
|
def __repr__(self):
|
|
14
|
-
return
|
|
24
|
+
return (
|
|
25
|
+
f"FileConfig(data_dir={self.data_dir},\n filename={self.filename},\n"
|
|
26
|
+
f"url={self.url},\n date_column={self.date_column},\n"
|
|
27
|
+
f" date_index={self.date_index})"
|
|
28
|
+
)
|
|
15
29
|
|
|
16
30
|
|
|
17
31
|
class DataConfig(BaseModel):
|
|
@@ -22,48 +22,38 @@ class Distribution:
|
|
|
22
22
|
return f"Distribution(name='{self.name}',\n dist={self.dist.__class__.__name__},\n is_discrete={self.is_discrete},\n num_params={self.num_params})"
|
|
23
23
|
|
|
24
24
|
|
|
25
|
-
@dataclass
|
|
26
25
|
class DistributionRegistry:
|
|
27
|
-
"""Registry for statistical distributions with metadata.
|
|
28
|
-
|
|
29
|
-
_instance = None
|
|
26
|
+
"""Registry for statistical distributions with metadata.
|
|
30
27
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
cls._instance = super().__new__(cls)
|
|
35
|
-
cls._instance.initialized = False
|
|
36
|
-
return cls._instance
|
|
28
|
+
Every instance starts from the shipped set of families, so registrations
|
|
29
|
+
are scoped to the registry you registered them on.
|
|
30
|
+
"""
|
|
37
31
|
|
|
38
32
|
def __init__(self):
|
|
39
33
|
"""Initialize the registry with common distributions."""
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
self.register_distribution(
|
|
64
|
-
"weibull", stats.weibull_min, is_discrete=False, num_params=3
|
|
65
|
-
)
|
|
66
|
-
self.initialized = True
|
|
34
|
+
self.distributions: Dict[str, Distribution] = {}
|
|
35
|
+
self.register_distribution("norm", stats.norm, is_discrete=False, num_params=2)
|
|
36
|
+
self.register_distribution(
|
|
37
|
+
"uniform", stats.uniform, is_discrete=False, num_params=2
|
|
38
|
+
)
|
|
39
|
+
self.register_distribution(
|
|
40
|
+
"lognorm", stats.lognorm, is_discrete=False, num_params=3
|
|
41
|
+
)
|
|
42
|
+
self.register_distribution(
|
|
43
|
+
"poisson", stats.poisson, is_discrete=True, num_params=1
|
|
44
|
+
)
|
|
45
|
+
self.register_distribution(
|
|
46
|
+
"exponential", stats.expon, is_discrete=False, num_params=2
|
|
47
|
+
)
|
|
48
|
+
self.register_distribution(
|
|
49
|
+
"skewnorm", stats.skewnorm, is_discrete=False, num_params=3
|
|
50
|
+
)
|
|
51
|
+
self.register_distribution(
|
|
52
|
+
"gamma", stats.gamma, is_discrete=False, num_params=3
|
|
53
|
+
)
|
|
54
|
+
self.register_distribution(
|
|
55
|
+
"weibull", stats.weibull_min, is_discrete=False, num_params=3
|
|
56
|
+
)
|
|
67
57
|
|
|
68
58
|
def __repr__(self) -> str:
|
|
69
59
|
"""Detailed representation of the registry."""
|