goad-toolkit 0.2.2__tar.gz → 0.2.3__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 (101) hide show
  1. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/MCP_SERVER.md +7 -7
  2. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/PKG-INFO +3 -2
  3. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/docs/02-pipelines.md +5 -7
  4. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/docs/03-plot-composition.md +7 -3
  5. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/docs/04-five-families.md +6 -4
  6. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/docs/08-api-reference.md +16 -2
  7. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/pyproject.toml +2 -1
  8. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/src/goad_toolkit/datatransforms.py +40 -1
  9. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/src/goad_toolkit/visualizer.py +114 -0
  10. goad_toolkit-0.2.3/tests/test_datatransforms.py +66 -0
  11. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/tests/test_visualizer.py +36 -0
  12. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/uv.lock +48 -1
  13. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/.claude/settings.local.json +0 -0
  14. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/.claude/worktrees/quizzical-solomon-b62511/.gitignore +0 -0
  15. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/.claude/worktrees/quizzical-solomon-b62511/.python-version +0 -0
  16. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/.claude/worktrees/quizzical-solomon-b62511/CHANGELOG.md +0 -0
  17. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/.claude/worktrees/quizzical-solomon-b62511/MCP_SERVER.md +0 -0
  18. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/.claude/worktrees/quizzical-solomon-b62511/README.md +0 -0
  19. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/.claude/worktrees/quizzical-solomon-b62511/demo/linear.py +0 -0
  20. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/.claude/worktrees/quizzical-solomon-b62511/docs/01-goal-oriented-analysis.md +0 -0
  21. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/.claude/worktrees/quizzical-solomon-b62511/docs/02-pipelines.md +0 -0
  22. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/.claude/worktrees/quizzical-solomon-b62511/docs/03-plot-composition.md +0 -0
  23. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/.claude/worktrees/quizzical-solomon-b62511/docs/04-five-families.md +0 -0
  24. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/.claude/worktrees/quizzical-solomon-b62511/docs/05-distributions.md +0 -0
  25. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/.claude/worktrees/quizzical-solomon-b62511/docs/06-models-and-residuals.md +0 -0
  26. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/.claude/worktrees/quizzical-solomon-b62511/docs/07-visual-critique.md +0 -0
  27. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/.claude/worktrees/quizzical-solomon-b62511/docs/08-api-reference.md +0 -0
  28. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/.claude/worktrees/quizzical-solomon-b62511/docs/09-analysis-method.md +0 -0
  29. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/.claude/worktrees/quizzical-solomon-b62511/docs/10-teaching-path.md +0 -0
  30. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/.claude/worktrees/quizzical-solomon-b62511/docs/README.md +0 -0
  31. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/.claude/worktrees/quizzical-solomon-b62511/goad_mcp.py +0 -0
  32. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/.claude/worktrees/quizzical-solomon-b62511/img/distribution_fit.png +0 -0
  33. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/.claude/worktrees/quizzical-solomon-b62511/img/goaded.png +0 -0
  34. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/.claude/worktrees/quizzical-solomon-b62511/img/linear_results.png +0 -0
  35. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/.claude/worktrees/quizzical-solomon-b62511/img/residuals.png +0 -0
  36. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/.claude/worktrees/quizzical-solomon-b62511/img/zscores.png +0 -0
  37. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/.claude/worktrees/quizzical-solomon-b62511/pyproject.toml +0 -0
  38. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/.claude/worktrees/quizzical-solomon-b62511/src/goad_toolkit/__init__.py +0 -0
  39. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/.claude/worktrees/quizzical-solomon-b62511/src/goad_toolkit/analytics.py +0 -0
  40. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/.claude/worktrees/quizzical-solomon-b62511/src/goad_toolkit/cli.py +0 -0
  41. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/.claude/worktrees/quizzical-solomon-b62511/src/goad_toolkit/config.py +0 -0
  42. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/.claude/worktrees/quizzical-solomon-b62511/src/goad_toolkit/dataprocessor.py +0 -0
  43. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/.claude/worktrees/quizzical-solomon-b62511/src/goad_toolkit/datatransforms.py +0 -0
  44. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/.claude/worktrees/quizzical-solomon-b62511/src/goad_toolkit/distributions.py +0 -0
  45. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/.claude/worktrees/quizzical-solomon-b62511/src/goad_toolkit/filehandler.py +0 -0
  46. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/.claude/worktrees/quizzical-solomon-b62511/src/goad_toolkit/models.py +0 -0
  47. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/.claude/worktrees/quizzical-solomon-b62511/src/goad_toolkit/visualizer.py +0 -0
  48. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/.claude/worktrees/quizzical-solomon-b62511/tests/test_cli.py +0 -0
  49. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/.claude/worktrees/quizzical-solomon-b62511/tests/test_distributions.py +0 -0
  50. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/.claude/worktrees/quizzical-solomon-b62511/tests/test_filehandler.py +0 -0
  51. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/.claude/worktrees/quizzical-solomon-b62511/tests/test_visualizer.py +0 -0
  52. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/.claude/worktrees/quizzical-solomon-b62511/uv.lock +0 -0
  53. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/.gitignore +0 -0
  54. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/.python-version +0 -0
  55. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/.remember/.gitignore +0 -0
  56. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/.remember/logs/autonomous/save-195516.log +0 -0
  57. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/.remember/logs/autonomous/save-195720.log +0 -0
  58. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/.remember/logs/autonomous/save-195933.log +0 -0
  59. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/.remember/logs/autonomous/save-200831.log +0 -0
  60. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/.remember/logs/autonomous/save-201034.log +0 -0
  61. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/.remember/logs/hook-errors.log +0 -0
  62. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/.remember/logs/memory-2026-08-10.log +0 -0
  63. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/.remember/now.md +0 -0
  64. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/.remember/tmp/capture-alive +0 -0
  65. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/.remember/tmp/capture-alive.d/90574e63-97cd-4c11-87e5-23b656c56fd9 +0 -0
  66. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/.remember/tmp/case-divergence +0 -0
  67. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/.remember/tmp/last-ndc.ts +0 -0
  68. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/.remember/tmp/last-save-ts +0 -0
  69. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/.remember/tmp/last-save.json +0 -0
  70. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/.remember/tmp/now-day +0 -0
  71. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/.remember/tmp/post-tool-ran +0 -0
  72. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/.remember/tmp/save-session.pid +0 -0
  73. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/.remember/tmp/session-slug +0 -0
  74. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/.remember/today-2026-08-10.md +0 -0
  75. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/CHANGELOG.md +0 -0
  76. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/README.md +0 -0
  77. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/demo/linear.py +0 -0
  78. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/docs/01-goal-oriented-analysis.md +0 -0
  79. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/docs/05-distributions.md +0 -0
  80. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/docs/06-models-and-residuals.md +0 -0
  81. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/docs/07-visual-critique.md +0 -0
  82. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/docs/09-analysis-method.md +0 -0
  83. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/docs/10-teaching-path.md +0 -0
  84. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/docs/README.md +0 -0
  85. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/goad_mcp.py +0 -0
  86. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/img/distribution_fit.png +0 -0
  87. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/img/goaded.png +0 -0
  88. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/img/linear_results.png +0 -0
  89. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/img/residuals.png +0 -0
  90. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/img/zscores.png +0 -0
  91. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/src/goad_toolkit/__init__.py +0 -0
  92. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/src/goad_toolkit/analytics.py +0 -0
  93. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/src/goad_toolkit/cli.py +0 -0
  94. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/src/goad_toolkit/config.py +0 -0
  95. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/src/goad_toolkit/dataprocessor.py +0 -0
  96. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/src/goad_toolkit/distributions.py +0 -0
  97. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/src/goad_toolkit/filehandler.py +0 -0
  98. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/src/goad_toolkit/models.py +0 -0
  99. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/tests/test_cli.py +0 -0
  100. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/tests/test_distributions.py +0 -0
  101. {goad_toolkit-0.2.2 → goad_toolkit-0.2.3}/tests/test_filehandler.py +0 -0
@@ -103,13 +103,13 @@ You pin a version with `GOAD_REF` — a git ref (branch, tag, or commit). That o
103
103
  selects both the script (in the URL) and the docs it fetches (inside the script), so a whole
104
104
  cohort runs exactly the same thing and the two cannot drift apart.
105
105
 
106
- > **Note:** the docs and this server land in **v0.2.0**. Until that tag exists, point
107
- > `GOAD_REF` at the branch (`feat/docs-and-mcp`) or work from a local clone (see below).
106
+ > **Note:** the docs and this server land in **v0.2.2**.
107
+ > You can also point `GOAD_REF` to a branch or work from a local clone (see below).
108
108
 
109
109
  ### Claude Code
110
110
 
111
111
  ```bash
112
- claude mcp add goad -e GOAD_REF=v0.2.0 -- \
112
+ claude mcp add goad -e GOAD_REF=v0.2.2 -- \
113
113
  sh -c 'uv run --no-project https://raw.githubusercontent.com/raoulg/goad_toolkit/$GOAD_REF/goad_mcp.py'
114
114
  ```
115
115
 
@@ -123,7 +123,7 @@ Add to `.cursor/mcp.json` (per-project) or `~/.cursor/mcp.json` (global):
123
123
  "goad": {
124
124
  "command": "sh",
125
125
  "args": ["-c", "uv run --no-project https://raw.githubusercontent.com/raoulg/goad_toolkit/$GOAD_REF/goad_mcp.py"],
126
- "env": { "GOAD_REF": "v0.2.0" }
126
+ "env": { "GOAD_REF": "v0.2.2" }
127
127
  }
128
128
  }
129
129
  }
@@ -137,7 +137,7 @@ Add to `~/.codex/config.toml`:
137
137
  [mcp_servers.goad]
138
138
  command = "sh"
139
139
  args = ["-c", "uv run --no-project https://raw.githubusercontent.com/raoulg/goad_toolkit/$GOAD_REF/goad_mcp.py"]
140
- env = { GOAD_REF = "v0.2.0" }
140
+ env = { GOAD_REF = "v0.2.2" }
141
141
  ```
142
142
 
143
143
  ### Claude Desktop
@@ -151,7 +151,7 @@ app:
151
151
  "goad": {
152
152
  "command": "sh",
153
153
  "args": ["-c", "uv run --no-project https://raw.githubusercontent.com/raoulg/goad_toolkit/$GOAD_REF/goad_mcp.py"],
154
- "env": { "GOAD_REF": "v0.2.0" }
154
+ "env": { "GOAD_REF": "v0.2.2" }
155
155
  }
156
156
  }
157
157
  }
@@ -187,7 +187,7 @@ add with the new tag:
187
187
 
188
188
  ```bash
189
189
  claude mcp remove goad -s user
190
- claude mcp add goad -s user -e GOAD_REF=v0.2.0 -- \
190
+ claude mcp add goad -s user -e GOAD_REF=v0.2.2 -- \
191
191
  sh -c 'uv run --no-project https://raw.githubusercontent.com/raoulg/goad_toolkit/$GOAD_REF/goad_mcp.py'
192
192
  ```
193
193
 
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: goad-toolkit
3
- Version: 0.2.2
3
+ Version: 0.2.3
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>
@@ -13,6 +13,7 @@ Requires-Dist: pydantic>=2.10.6
13
13
  Requires-Dist: requests>=2.32.3
14
14
  Requires-Dist: scipy>=1.15.2
15
15
  Requires-Dist: seaborn>=0.13.2
16
+ Requires-Dist: statsmodels>=0.14.4
16
17
  Requires-Dist: tqdm>=4.67.1
17
18
  Provides-Extra: parquet
18
19
  Requires-Dist: pyarrow>=19.0.0; extra == 'parquet'
@@ -58,6 +58,7 @@ input frame is safe, and the steps stay cheap.
58
58
  | `SelectDataRange` | keep rows in a date range | `start_date`, `end_date` |
59
59
  | `RollingAvg` | rolling mean, drops the leading NaNs | `column`, `window`, `rename` |
60
60
  | `ZScaler` | standardise to mean 0, std 1 | `column`, `rename` |
61
+ | `TimeFeatures` | derive calendar columns from a timestamp | `column`, `features` |
61
62
 
62
63
  `rename=True` writes to a new column (`deaths_shifted`, `deaths_zscore`, …) instead of
63
64
  overwriting. Prefer it. An overwritten column is a step you cannot debug, and the whole
@@ -75,20 +76,17 @@ Subclass `TransformBase` and implement `transform`. That is the entire contract.
75
76
  from goad_toolkit.datatransforms import TransformBase
76
77
  import pandas as pd
77
78
 
78
- class TimeFeatures(TransformBase):
79
- """Derive calendar features from a timestamp column."""
79
+ class IsWeekend(TransformBase):
80
+ """Flag Saturday/Sunday from a timestamp column."""
80
81
 
81
82
  def transform(self, data: pd.DataFrame, column: str) -> pd.DataFrame:
82
83
  ts = pd.to_datetime(data[column])
83
- data["date"] = ts.dt.date
84
- data["hour"] = ts.dt.hour
85
- data["day_name"] = ts.dt.day_name()
86
- data["isoweek"] = ts.dt.isocalendar().week
84
+ data["is_weekend"] = ts.dt.dayofweek >= 5
87
85
  return data
88
86
  ```
89
87
 
90
88
  ```python
91
- pipeline.add(TimeFeatures, column="timestamp")
89
+ pipeline.add(IsWeekend, column="timestamp")
92
90
  ```
93
91
 
94
92
  What the base class does for you:
@@ -151,6 +151,8 @@ step 3.
151
151
  | `ComparePlotDate` | `ComparePlot` + `VerticalDate` | |
152
152
  | `BarWithDates` | bars with a month locator on the x axis | |
153
153
  | `ResidualPlot` | `BarWithDates` + `VerticalDate` | the residual-over-time view |
154
+ | `DecomposePlot` | `seasonal_decompose`, one panel each | observed/trend/seasonal/residual, overrides `plot` |
155
+ | `ACFPlot` | `acf` as bars with a confidence band | see [Five families §4.2](04-five-families.md) |
154
156
  | `HistogramPlot` | `sns.histplot`, `stat="density"` | density so a pdf can be overlaid |
155
157
  | `DistPlot` | a scipy distribution's pdf or pmf | estimates its own x-range from `ppf` |
156
158
  | `PlotFits` | histogram + fitted pdf, one panel per fit | see [Distributions](05-distributions.md) |
@@ -216,9 +218,11 @@ lines of code.
216
218
  `self.ax.axvline(...)`, `self.fig.suptitle(...)`. Keeping to them is what makes
217
219
  `plot_on_axes` reliable.
218
220
 
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.
221
+ `PlotFits` and `DecomposePlot` override `plot` rather than `build`, because both need to know
222
+ how many panels to create before the figure exists variable for `PlotFits` (one per fit),
223
+ fixed at four for `DecomposePlot`. Either way, `build` alone cannot decide the panel count
224
+ before `create_figure` runs, so both leave it as a no-op. This is the exception, not a pattern
225
+ to copy for a plot that only ever needs one axis.
222
226
 
223
227
  ---
224
228
 
@@ -76,10 +76,12 @@ the period is long relative to your intuition.
76
76
  the same data support different stories. Choose the window from the mechanism you are
77
77
  studying, before you look.
78
78
 
79
- **In GOAD:** `ShiftValues`, `DiffValues`, `RollingAvg`, `SelectDataRange` are all pipeline
80
- steps, so the smoothing window is a named parameter you can change in one line and rerun.
81
- `ComparePlot` puts raw and smoothed on the same axes; `VerticalDate` marks the moment you
82
- claim something changed, with a label, on the plot.
79
+ **In GOAD:** `ShiftValues`, `DiffValues`, `RollingAvg`, `SelectDataRange`, `TimeFeatures` are
80
+ all pipeline steps, so the smoothing window is a named parameter you can change in one line
81
+ and rerun. `ComparePlot` puts raw and smoothed on the same axes; `VerticalDate` marks the
82
+ moment you claim something changed, with a label, on the plot. `DecomposePlot` wraps
83
+ `seasonal_decompose` into the four-panel observed/trend/seasonal/residual view; `ACFPlot`
84
+ wraps `acf` with its confidence band, for finding a period you did not already suspect.
83
85
 
84
86
  ---
85
87
 
@@ -83,10 +83,15 @@ class TransformBase(ABC):
83
83
  | `SelectDataRange` | `start_date: str, end_date: str` |
84
84
  | `RollingAvg` | `column: str, window: int, rename: bool = False` |
85
85
  | `ZScaler` | `column: str, rename: bool = False` |
86
+ | `TimeFeatures` | `column: str, features: Optional[List[str]] = None` |
86
87
 
87
88
  `rename=True` writes to `{column}_shifted` / `_diff` / `_rolling_avg` / `_zscore` instead of
88
89
  overwriting.
89
90
 
91
+ `TimeFeatures.AVAILABLE` is `("date", "hour", "day_name", "isoweek", "year_week")`;
92
+ `TimeFeatures.DEFAULT` — used when `features` is omitted — is `("day_name", "isoweek",
93
+ "year_week")`. A feature named the same as `column` overwrites it.
94
+
90
95
  ```python
91
96
  class Pipeline:
92
97
  def add(self, transform_class: Type[T], name: Optional[str] = None, **kwargs) -> "Pipeline"
@@ -267,12 +272,17 @@ class BasePlot(ABC):
267
272
  | `ResidualPlot` | `data, x, y, date, datelabel, interval: int = 1` |
268
273
  | `HistogramPlot` | `data: np.ndarray, bins=None, kde=False, color="skyblue", alpha=0.7, **kwargs` |
269
274
  | `DistPlot` | `distribution, x_range=None, samples=1000, color="crimson", linewidth=2, label=None, **kwargs` |
275
+ | `ACFPlot` | `data, nlags=40, alpha=0.05, color="steelblue", band_color="gray", **kwargs` |
270
276
 
271
277
  ```python
272
278
  class PlotFits(BasePlot):
273
279
  def plot(self, data: np.ndarray,
274
280
  fit_results: List[Result],
275
281
  fitplotsettings: FitPlotSettings) -> Figure
282
+
283
+ class DecomposePlot(BasePlot):
284
+ def plot(self, data: pd.DataFrame, column: str, period: int,
285
+ model: str = "additive", **kwargs) -> Figure
276
286
  ```
277
287
 
278
288
  Every `build` returns `(self.fig, self.ax)`, and every plot draws on `self.ax` / `self.fig`
@@ -309,12 +319,16 @@ of `likelihood`, `ks`, `combined`; `--method` is passed to `scipy.stats.fit`.
309
319
 
310
320
  ## 8.10 Rough edges worth knowing
311
321
 
312
- - `PlotFits` overrides `plot` rather than `build`; its `build` is a no-op. It is the one
313
- exception to the `build` contract.
322
+ - `PlotFits` and `DecomposePlot` override `plot` rather than `build`; their `build` is a
323
+ no-op. Both need the panel count decided before `create_figure` runs — from the fit results
324
+ for `PlotFits`, always four for `DecomposePlot` — which `build` alone cannot do.
314
325
  - Fits that fail come back as `FailedFit` values in the result list rather than raising, so
315
326
  a list of results can contain both kinds and needs an `isinstance` check.
316
327
  - `Pipeline.apply` copies the frame once and then mutates that copy in place, so transforms
317
328
  see each other's columns — the order you add steps in matters.
329
+ - `ACFPlot` raises `ValueError` if the data contains NaNs, rather than passing them through.
330
+ `statsmodels.tsa.stattools.acf` does not raise on missing values — it silently returns an
331
+ all-NaN result — so a decomposition's residual needs `.dropna()` before it reaches `ACFPlot`.
318
332
 
319
333
  ---
320
334
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "goad-toolkit"
3
- version = "0.2.2"
3
+ version = "0.2.3"
4
4
  description = "An extensible toolkit for Goal Oriented Analysis of Data"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -16,6 +16,7 @@ dependencies = [
16
16
  "requests>=2.32.3",
17
17
  "scipy>=1.15.2",
18
18
  "seaborn>=0.13.2",
19
+ "statsmodels>=0.14.4",
19
20
  "tqdm>=4.67.1",
20
21
  ]
21
22
  urls = {Github = "https://github.com/raoulg/goad_toolkit"}
@@ -1,5 +1,5 @@
1
1
  from abc import ABC, abstractmethod
2
- from typing import Any, Dict, Optional, Type, TypeVar
2
+ from typing import Any, Dict, List, Optional, Type, TypeVar
3
3
 
4
4
  import pandas as pd
5
5
  from tqdm import tqdm
@@ -122,6 +122,45 @@ class ZScaler(TransformBase):
122
122
  return data
123
123
 
124
124
 
125
+ class TimeFeatures(TransformBase):
126
+ """Derive calendar features from a timestamp column.
127
+
128
+ `features` picks which columns to write, from `"date"`, `"hour"`,
129
+ `"day_name"`, `"isoweek"`, `"year_week"`. Defaults to `"day_name"`,
130
+ `"isoweek"`, `"year_week"` — the three lesson 1 already relies on.
131
+ A feature named the same as `column` overwrites it.
132
+ """
133
+
134
+ AVAILABLE = ("date", "hour", "day_name", "isoweek", "year_week")
135
+ DEFAULT = ("day_name", "isoweek", "year_week")
136
+
137
+ def transform(
138
+ self,
139
+ data: pd.DataFrame,
140
+ column: str,
141
+ features: Optional[List[str]] = None,
142
+ ) -> pd.DataFrame:
143
+ selected = list(features) if features is not None else list(self.DEFAULT)
144
+ unknown = set(selected) - set(self.AVAILABLE)
145
+ if unknown:
146
+ raise ValueError(
147
+ f"Unknown feature(s) {sorted(unknown)}; choose from {self.AVAILABLE}"
148
+ )
149
+
150
+ ts = pd.to_datetime(data[column])
151
+ if "date" in selected:
152
+ data["date"] = ts.dt.date
153
+ if "hour" in selected:
154
+ data["hour"] = ts.dt.hour
155
+ if "day_name" in selected:
156
+ data["day_name"] = ts.dt.day_name()
157
+ if "isoweek" in selected:
158
+ data["isoweek"] = ts.dt.isocalendar().week
159
+ if "year_week" in selected:
160
+ data["year_week"] = ts.dt.strftime("%Y-%W")
161
+ return data
162
+
163
+
125
164
  class Pipeline:
126
165
  """Pipeline for chaining data transformations."""
127
166
 
@@ -11,6 +11,8 @@ import seaborn as sns
11
11
  from loguru import logger
12
12
  from matplotlib.axes import Axes
13
13
  from matplotlib.figure import Figure
14
+ from statsmodels.tsa.seasonal import seasonal_decompose
15
+ from statsmodels.tsa.stattools import acf
14
16
 
15
17
  from goad_toolkit.analytics import FitResult, Result
16
18
 
@@ -248,6 +250,118 @@ class ResidualPlot(BasePlot):
248
250
  return self.fig, self.ax
249
251
 
250
252
 
253
+ class DecomposePlot(BasePlot):
254
+ """Decompose a series into observed, trend, seasonal and residual panels."""
255
+
256
+ def plot(
257
+ self,
258
+ data: pd.DataFrame,
259
+ column: str,
260
+ period: int,
261
+ model: str = "additive",
262
+ **kwargs,
263
+ ) -> Figure:
264
+ """
265
+ Run `statsmodels.tsa.seasonal.seasonal_decompose` and draw each
266
+ component on its own panel.
267
+
268
+ Parameters:
269
+ -----------
270
+ data : pd.DataFrame
271
+ Frame with a datetime index and the series to decompose.
272
+ column : str
273
+ Column to decompose.
274
+ period : int
275
+ Number of observations per cycle (e.g. 24 for hourly data with a
276
+ daily cycle, 7 for daily data with a weekly cycle).
277
+ model : str
278
+ `"additive"` or `"multiplicative"`, passed to `seasonal_decompose`.
279
+ **kwargs : Additional keyword arguments passed to `seasonal_decompose`.
280
+
281
+ Returns:
282
+ --------
283
+ fig : The created figure, with one panel per component.
284
+ """
285
+ result = seasonal_decompose(data[column], model=model, period=period, **kwargs)
286
+ components = [
287
+ ("Observed", result.observed),
288
+ ("Trend", result.trend),
289
+ ("Seasonal", result.seasonal),
290
+ ("Residual", result.resid),
291
+ ]
292
+
293
+ self.settings.subplot_titles = [name for name, _ in components]
294
+ self.fig, axes = self.create_figure(n_plots=len(components))
295
+
296
+ for ax, (_, values) in zip(axes, components):
297
+ comp_data = values.rename("value").reset_index()
298
+ index_col = comp_data.columns[0]
299
+ line = LinePlot(self.settings)
300
+ self.plot_on_axes(line, ax=ax, data=comp_data, x=index_col, y="value")
301
+
302
+ return self.fig
303
+
304
+ def build(self):
305
+ """No-op: `DecomposePlot` overrides `plot` because the panel count is fixed at four."""
306
+ pass
307
+
308
+
309
+ class ACFPlot(BasePlot):
310
+ """Bar plot of the autocorrelation function with a Bartlett confidence band."""
311
+
312
+ def build(
313
+ self,
314
+ data: Any,
315
+ nlags: int = 40,
316
+ alpha: float = 0.05,
317
+ color: str = "steelblue",
318
+ band_color: str = "gray",
319
+ **kwargs,
320
+ ):
321
+ """
322
+ Plot the autocorrelation function for a series.
323
+
324
+ Parameters:
325
+ -----------
326
+ data : array-like
327
+ Series to compute the ACF for. Must not contain missing values —
328
+ drop NaNs first (a decomposition's residual has them at the edges).
329
+ nlags : int
330
+ Number of lags to compute and plot.
331
+ alpha : float
332
+ Significance level for the confidence band.
333
+ color : str
334
+ Bar color.
335
+ band_color : str
336
+ Confidence band color.
337
+ **kwargs : Additional keyword arguments passed to `ax.bar`.
338
+
339
+ Returns:
340
+ --------
341
+ fig, ax : The created figure and axes.
342
+ """
343
+ if self.ax is None:
344
+ raise ValueError("No axes available for plotting")
345
+
346
+ series = pd.Series(data)
347
+ if series.isna().any():
348
+ raise ValueError(
349
+ "ACFPlot got missing values; statsmodels.tsa.stattools.acf does not "
350
+ "raise on NaN, it silently returns an all-NaN result. Drop NaNs first "
351
+ "(a decomposition's residual has them at the edges)."
352
+ )
353
+
354
+ values, confint = acf(series, nlags=nlags, alpha=alpha, fft=True) # type: ignore
355
+ lags = np.arange(len(values))
356
+ band = (confint[:, 1] - confint[:, 0]) / 2
357
+
358
+ self.ax.fill_between(lags, -band, band, color=band_color, alpha=0.2)
359
+ self.ax.axhline(0, color="black", linewidth=0.8)
360
+ self.ax.bar(lags, values, color=color, width=0.3, **kwargs)
361
+
362
+ return self.fig, self.ax
363
+
364
+
251
365
  class HistogramPlot(BasePlot):
252
366
  """Plot a histogram using seaborn."""
253
367
 
@@ -0,0 +1,66 @@
1
+ import pandas as pd
2
+ import pytest
3
+
4
+ from goad_toolkit.datatransforms import Pipeline, TimeFeatures
5
+
6
+
7
+ @pytest.fixture
8
+ def data() -> pd.DataFrame:
9
+ # 2024-01-01 is a Monday
10
+ timestamps = pd.date_range("2024-01-01 08:00", periods=10, freq="6h")
11
+ return pd.DataFrame({"timestamp": timestamps, "value": range(10)})
12
+
13
+
14
+ def test_default_features_match_lesson_one(data):
15
+ result = TimeFeatures(column="timestamp")(data.copy())
16
+
17
+ assert "day_name" in result.columns
18
+ assert "isoweek" in result.columns
19
+ assert "year_week" in result.columns
20
+ assert "date" not in result.columns
21
+ assert "hour" not in result.columns
22
+
23
+
24
+ def test_selected_features_only(data):
25
+ result = TimeFeatures(column="timestamp", features=["date", "hour"])(data.copy())
26
+
27
+ assert "date" in result.columns
28
+ assert "hour" in result.columns
29
+ assert "day_name" not in result.columns
30
+ assert result["hour"].tolist() == [8, 14, 20, 2, 8, 14, 20, 2, 8, 14]
31
+
32
+
33
+ def test_all_features(data):
34
+ result = TimeFeatures(
35
+ column="timestamp",
36
+ features=list(TimeFeatures.AVAILABLE),
37
+ )(data.copy())
38
+
39
+ for feature in TimeFeatures.AVAILABLE:
40
+ assert feature in result.columns
41
+ assert result["day_name"].iloc[0] == "Monday"
42
+ assert result["isoweek"].iloc[0] == 1
43
+
44
+
45
+ def test_unknown_feature_raises(data):
46
+ with pytest.raises(ValueError, match="Unknown feature"):
47
+ TimeFeatures(column="timestamp", features=["not_a_feature"])(data.copy())
48
+
49
+
50
+ def test_feature_overwrites_same_named_source_column():
51
+ # mirrors lesson 1's own pipeline: source column is called "date"
52
+ df = pd.DataFrame({"date": pd.date_range("2024-01-01", periods=3, freq="D")})
53
+
54
+ result = TimeFeatures(column="date", features=["date"])(df)
55
+
56
+ assert result["date"].iloc[0] == pd.Timestamp("2024-01-01").date()
57
+
58
+
59
+ def test_works_in_a_pipeline(data):
60
+ pipeline = Pipeline()
61
+ pipeline.add(TimeFeatures, column="timestamp", features=["day_name", "hour"])
62
+
63
+ result = pipeline.apply(data)
64
+
65
+ assert "day_name" in result.columns
66
+ assert "hour" in result.columns
@@ -7,9 +7,11 @@ import pandas as pd # noqa: E402
7
7
  import pytest # noqa: E402
8
8
 
9
9
  from goad_toolkit.visualizer import ( # noqa: E402
10
+ ACFPlot,
10
11
  BarWithDates,
11
12
  ComparePlot,
12
13
  ComparePlotDate,
14
+ DecomposePlot,
13
15
  LinePlot,
14
16
  PlotSettings,
15
17
  ResidualPlot,
@@ -111,3 +113,37 @@ def test_compare_plot_date_returns_fig_and_ax(data):
111
113
 
112
114
  assert fig is not None
113
115
  assert ax is not None
116
+
117
+
118
+ @pytest.fixture
119
+ def weekly_cycle() -> pd.DataFrame:
120
+ dates = pd.date_range("2021-01-01", periods=56, freq="D")
121
+ day_of_week = dates.dayofweek.to_numpy()
122
+ seasonal = [0, 1, 2, 3, 4, -8, -8][:7] # weekday-shaped, weekend dip
123
+ values = [10 + i * 0.1 + seasonal[d] for i, d in enumerate(day_of_week)]
124
+ return pd.DataFrame({"value": values}, index=dates)
125
+
126
+
127
+ def test_decompose_plot_returns_one_panel_per_component(weekly_cycle):
128
+ fig = DecomposePlot(PlotSettings()).plot(
129
+ data=weekly_cycle, column="value", period=7
130
+ )
131
+
132
+ visible_axes = [ax for ax in fig.axes if ax.get_visible()]
133
+ assert len(visible_axes) == 4
134
+ titles = [ax.get_title() for ax in visible_axes]
135
+ assert titles == ["Observed", "Trend", "Seasonal", "Residual"]
136
+
137
+
138
+ def test_acf_plot_returns_fig_and_ax(weekly_cycle):
139
+ fig, ax = ACFPlot(PlotSettings()).plot(data=weekly_cycle["value"], nlags=14)
140
+
141
+ assert fig is not None
142
+ assert len(ax.patches) == 15 # lags 0..14 inclusive
143
+
144
+
145
+ def test_acf_plot_rejects_missing_values():
146
+ series = pd.Series([1.0, 2.0, float("nan"), 4.0, 5.0])
147
+
148
+ with pytest.raises(ValueError, match="missing values"):
149
+ ACFPlot(PlotSettings()).plot(data=series, nlags=2)
@@ -383,7 +383,7 @@ wheels = [
383
383
 
384
384
  [[package]]
385
385
  name = "goad-toolkit"
386
- version = "0.2.1"
386
+ version = "0.2.2"
387
387
  source = { editable = "." }
388
388
  dependencies = [
389
389
  { name = "loguru" },
@@ -394,6 +394,7 @@ dependencies = [
394
394
  { name = "requests" },
395
395
  { name = "scipy" },
396
396
  { name = "seaborn" },
397
+ { name = "statsmodels" },
397
398
  { name = "tqdm" },
398
399
  ]
399
400
 
@@ -423,6 +424,7 @@ requires-dist = [
423
424
  { name = "requests", specifier = ">=2.32.3" },
424
425
  { name = "scipy", specifier = ">=1.15.2" },
425
426
  { name = "seaborn", specifier = ">=0.13.2" },
427
+ { name = "statsmodels", specifier = ">=0.14.4" },
426
428
  { name = "tqdm", specifier = ">=4.67.1" },
427
429
  ]
428
430
  provides-extras = ["parquet"]
@@ -1236,6 +1238,18 @@ wheels = [
1236
1238
  { url = "https://files.pythonhosted.org/packages/c6/ac/dac4a63f978e4dcb3c6d3a78c4d8e0192a113d288502a1216950c41b1027/parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18", size = 103650, upload-time = "2024-04-05T09:43:53.299Z" },
1237
1239
  ]
1238
1240
 
1241
+ [[package]]
1242
+ name = "patsy"
1243
+ version = "1.0.2"
1244
+ source = { registry = "https://pypi.org/simple" }
1245
+ dependencies = [
1246
+ { name = "numpy" },
1247
+ ]
1248
+ sdist = { url = "https://files.pythonhosted.org/packages/be/44/ed13eccdd0519eff265f44b670d46fbb0ec813e2274932dc1c0e48520f7d/patsy-1.0.2.tar.gz", hash = "sha256:cdc995455f6233e90e22de72c37fcadb344e7586fb83f06696f54d92f8ce74c0", size = 399942, upload-time = "2025-10-20T16:17:37.535Z" }
1249
+ wheels = [
1250
+ { url = "https://files.pythonhosted.org/packages/f1/70/ba4b949bdc0490ab78d545459acd7702b211dfccf7eb89bbc1060f52818d/patsy-1.0.2-py2.py3-none-any.whl", hash = "sha256:37bfddbc58fcf0362febb5f54f10743f8b21dd2aa73dec7e7ef59d1b02ae668a", size = 233301, upload-time = "2025-10-20T16:17:36.563Z" },
1251
+ ]
1252
+
1239
1253
  [[package]]
1240
1254
  name = "pexpect"
1241
1255
  version = "4.9.0"
@@ -1857,6 +1871,39 @@ wheels = [
1857
1871
  { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" },
1858
1872
  ]
1859
1873
 
1874
+ [[package]]
1875
+ name = "statsmodels"
1876
+ version = "0.14.6"
1877
+ source = { registry = "https://pypi.org/simple" }
1878
+ dependencies = [
1879
+ { name = "numpy" },
1880
+ { name = "packaging" },
1881
+ { name = "pandas" },
1882
+ { name = "patsy" },
1883
+ { name = "scipy" },
1884
+ ]
1885
+ sdist = { url = "https://files.pythonhosted.org/packages/0d/81/e8d74b34f85285f7335d30c5e3c2d7c0346997af9f3debf9a0a9a63de184/statsmodels-0.14.6.tar.gz", hash = "sha256:4d17873d3e607d398b85126cd4ed7aad89e4e9d89fc744cdab1af3189a996c2a", size = 20689085, upload-time = "2025-12-05T23:08:39.522Z" }
1886
+ wheels = [
1887
+ { url = "https://files.pythonhosted.org/packages/25/ce/308e5e5da57515dd7cab3ec37ea2d5b8ff50bef1fcc8e6d31456f9fae08e/statsmodels-0.14.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fe76140ae7adc5ff0e60a3f0d56f4fffef484efa803c3efebf2fcd734d72ecb5", size = 10091932, upload-time = "2025-12-05T19:28:55.446Z" },
1888
+ { url = "https://files.pythonhosted.org/packages/05/30/affbabf3c27fb501ec7b5808230c619d4d1a4525c07301074eb4bda92fa9/statsmodels-0.14.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:26d4f0ed3b31f3c86f83a92f5c1f5cbe63fc992cd8915daf28ca49be14463a1c", size = 9997345, upload-time = "2025-12-05T19:29:10.278Z" },
1889
+ { url = "https://files.pythonhosted.org/packages/48/f5/3a73b51e6450c31652c53a8e12e24eac64e3824be816c0c2316e7dbdcb7d/statsmodels-0.14.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8c00a42863e4f4733ac9d078bbfad816249c01451740e6f5053ecc7db6d6368", size = 10058649, upload-time = "2025-12-05T23:10:12.775Z" },
1890
+ { url = "https://files.pythonhosted.org/packages/81/68/dddd76117df2ef14c943c6bbb6618be5c9401280046f4ddfc9fb4596a1b8/statsmodels-0.14.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:19b58cf7474aa9e7e3b0771a66537148b2df9b5884fbf156096c0e6c1ff0469d", size = 10339446, upload-time = "2025-12-05T23:10:28.503Z" },
1891
+ { url = "https://files.pythonhosted.org/packages/56/4a/dce451c74c4050535fac1ec0c14b80706d8fc134c9da22db3c8a0ec62c33/statsmodels-0.14.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:81e7dcc5e9587f2567e52deaff5220b175bf2f648951549eae5fc9383b62bc37", size = 10368705, upload-time = "2025-12-05T23:10:44.339Z" },
1892
+ { url = "https://files.pythonhosted.org/packages/60/15/3daba2df40be8b8a9a027d7f54c8dedf24f0d81b96e54b52293f5f7e3418/statsmodels-0.14.6-cp312-cp312-win_amd64.whl", hash = "sha256:b5eb07acd115aa6208b4058211138393a7e6c2cf12b6f213ede10f658f6a714f", size = 9543991, upload-time = "2025-12-05T23:10:58.536Z" },
1893
+ { url = "https://files.pythonhosted.org/packages/81/59/a5aad5b0cc266f5be013db8cde563ac5d2a025e7efc0c328d83b50c72992/statsmodels-0.14.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:47ee7af083623d2091954fa71c7549b8443168f41b7c5dce66510274c50fd73e", size = 10072009, upload-time = "2025-12-05T23:11:14.021Z" },
1894
+ { url = "https://files.pythonhosted.org/packages/53/dd/d8cfa7922fc6dc3c56fa6c59b348ea7de829a94cd73208c6f8202dd33f17/statsmodels-0.14.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa60d82e29fcd0a736e86feb63a11d2380322d77a9369a54be8b0965a3985f71", size = 9980018, upload-time = "2025-12-05T23:11:30.907Z" },
1895
+ { url = "https://files.pythonhosted.org/packages/ee/77/0ec96803eba444efd75dba32f2ef88765ae3e8f567d276805391ec2c98c6/statsmodels-0.14.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:89ee7d595f5939cc20bf946faedcb5137d975f03ae080f300ebb4398f16a5bd4", size = 10060269, upload-time = "2025-12-05T23:11:46.338Z" },
1896
+ { url = "https://files.pythonhosted.org/packages/10/b9/fd41f1f6af13a1a1212a06bb377b17762feaa6d656947bf666f76300fc05/statsmodels-0.14.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:730f3297b26749b216a06e4327fe0be59b8d05f7d594fb6caff4287b69654589", size = 10324155, upload-time = "2025-12-05T23:12:01.805Z" },
1897
+ { url = "https://files.pythonhosted.org/packages/ee/0f/a6900e220abd2c69cd0a07e3ad26c71984be6061415a60e0f17b152ecf08/statsmodels-0.14.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f1c08befa85e93acc992b72a390ddb7bd876190f1360e61d10cf43833463bc9c", size = 10349765, upload-time = "2025-12-05T23:12:18.018Z" },
1898
+ { url = "https://files.pythonhosted.org/packages/98/08/b79f0c614f38e566eebbdcff90c0bcacf3c6ba7a5bbb12183c09c29ca400/statsmodels-0.14.6-cp313-cp313-win_amd64.whl", hash = "sha256:8021271a79f35b842c02a1794465a651a9d06ec2080f76ebc3b7adce77d08233", size = 9540043, upload-time = "2025-12-05T23:12:33.887Z" },
1899
+ { url = "https://files.pythonhosted.org/packages/71/de/09540e870318e0c7b58316561d417be45eff731263b4234fdd2eee3511a8/statsmodels-0.14.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:00781869991f8f02ad3610da6627fd26ebe262210287beb59761982a8fa88cae", size = 10069403, upload-time = "2025-12-05T23:12:48.424Z" },
1900
+ { url = "https://files.pythonhosted.org/packages/ab/f0/63c1bfda75dc53cee858006e1f46bd6d6f883853bea1b97949d0087766ca/statsmodels-0.14.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:73f305fbf31607b35ce919fae636ab8b80d175328ed38fdc6f354e813b86ee37", size = 9989253, upload-time = "2025-12-05T23:13:05.274Z" },
1901
+ { url = "https://files.pythonhosted.org/packages/c1/98/b0dfb4f542b2033a3341aa5f1bdd97024230a4ad3670c5b0839d54e3dcab/statsmodels-0.14.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e443e7077a6e2d3faeea72f5a92c9f12c63722686eb80bb40a0f04e4a7e267ad", size = 10090802, upload-time = "2025-12-05T23:13:20.653Z" },
1902
+ { url = "https://files.pythonhosted.org/packages/34/0e/2408735aca9e764643196212f9069912100151414dd617d39ffc72d77eee/statsmodels-0.14.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3414e40c073d725007a6603a18247ab7af3467e1af4a5e5a24e4c27bc26673b4", size = 10337587, upload-time = "2025-12-05T23:13:37.597Z" },
1903
+ { url = "https://files.pythonhosted.org/packages/0f/36/4d44f7035ab3c0b2b6a4c4ebb98dedf36246ccbc1b3e2f51ebcd7ac83abb/statsmodels-0.14.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a518d3f9889ef920116f9fa56d0338069e110f823926356946dae83bc9e33e19", size = 10363350, upload-time = "2025-12-05T23:13:53.08Z" },
1904
+ { url = "https://files.pythonhosted.org/packages/26/33/f1652d0c59fa51de18492ee2345b65372550501ad061daa38f950be390b6/statsmodels-0.14.6-cp314-cp314-win_amd64.whl", hash = "sha256:151b73e29f01fe619dbce7f66d61a356e9d1fe5e906529b78807df9189c37721", size = 9588010, upload-time = "2025-12-05T23:14:07.28Z" },
1905
+ ]
1906
+
1860
1907
  [[package]]
1861
1908
  name = "terminado"
1862
1909
  version = "0.18.1"
File without changes
File without changes
File without changes
File without changes