goad-toolkit 0.2.4__tar.gz → 0.2.5__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.4 → goad_toolkit-0.2.5}/PKG-INFO +1 -1
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/docs/02-pipelines.md +26 -25
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/docs/08-api-reference.md +7 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/pyproject.toml +1 -1
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/src/goad_toolkit/datatransforms.py +47 -0
- goad_toolkit-0.2.5/tests/test_datatransforms.py +167 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/uv.lock +1 -1
- goad_toolkit-0.2.4/tests/test_datatransforms.py +0 -66
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.claude/settings.local.json +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.claude/worktrees/quizzical-solomon-b62511/.gitignore +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.claude/worktrees/quizzical-solomon-b62511/.python-version +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.claude/worktrees/quizzical-solomon-b62511/CHANGELOG.md +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.claude/worktrees/quizzical-solomon-b62511/MCP_SERVER.md +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.claude/worktrees/quizzical-solomon-b62511/README.md +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.claude/worktrees/quizzical-solomon-b62511/demo/linear.py +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.claude/worktrees/quizzical-solomon-b62511/docs/01-goal-oriented-analysis.md +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.claude/worktrees/quizzical-solomon-b62511/docs/02-pipelines.md +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.claude/worktrees/quizzical-solomon-b62511/docs/03-plot-composition.md +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.claude/worktrees/quizzical-solomon-b62511/docs/04-five-families.md +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.claude/worktrees/quizzical-solomon-b62511/docs/05-distributions.md +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.claude/worktrees/quizzical-solomon-b62511/docs/06-models-and-residuals.md +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.claude/worktrees/quizzical-solomon-b62511/docs/07-visual-critique.md +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.claude/worktrees/quizzical-solomon-b62511/docs/08-api-reference.md +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.claude/worktrees/quizzical-solomon-b62511/docs/09-analysis-method.md +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.claude/worktrees/quizzical-solomon-b62511/docs/10-teaching-path.md +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.claude/worktrees/quizzical-solomon-b62511/docs/README.md +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.claude/worktrees/quizzical-solomon-b62511/goad_mcp.py +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.claude/worktrees/quizzical-solomon-b62511/img/distribution_fit.png +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.claude/worktrees/quizzical-solomon-b62511/img/goaded.png +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.claude/worktrees/quizzical-solomon-b62511/img/linear_results.png +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.claude/worktrees/quizzical-solomon-b62511/img/residuals.png +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.claude/worktrees/quizzical-solomon-b62511/img/zscores.png +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.claude/worktrees/quizzical-solomon-b62511/pyproject.toml +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.claude/worktrees/quizzical-solomon-b62511/src/goad_toolkit/__init__.py +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.claude/worktrees/quizzical-solomon-b62511/src/goad_toolkit/analytics.py +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.claude/worktrees/quizzical-solomon-b62511/src/goad_toolkit/cli.py +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.claude/worktrees/quizzical-solomon-b62511/src/goad_toolkit/config.py +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.claude/worktrees/quizzical-solomon-b62511/src/goad_toolkit/dataprocessor.py +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.claude/worktrees/quizzical-solomon-b62511/src/goad_toolkit/datatransforms.py +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.claude/worktrees/quizzical-solomon-b62511/src/goad_toolkit/distributions.py +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.claude/worktrees/quizzical-solomon-b62511/src/goad_toolkit/filehandler.py +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.claude/worktrees/quizzical-solomon-b62511/src/goad_toolkit/models.py +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.claude/worktrees/quizzical-solomon-b62511/src/goad_toolkit/visualizer.py +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.claude/worktrees/quizzical-solomon-b62511/tests/test_cli.py +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.claude/worktrees/quizzical-solomon-b62511/tests/test_distributions.py +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.claude/worktrees/quizzical-solomon-b62511/tests/test_filehandler.py +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.claude/worktrees/quizzical-solomon-b62511/tests/test_visualizer.py +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.claude/worktrees/quizzical-solomon-b62511/uv.lock +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.gitignore +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.python-version +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.remember/.gitignore +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.remember/logs/autonomous/save-195516.log +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.remember/logs/autonomous/save-195720.log +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.remember/logs/autonomous/save-195933.log +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.remember/logs/autonomous/save-200831.log +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.remember/logs/autonomous/save-201034.log +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.remember/logs/hook-errors.log +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.remember/logs/memory-2026-08-10.log +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.remember/now.md +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.remember/tmp/capture-alive +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.remember/tmp/capture-alive.d/90574e63-97cd-4c11-87e5-23b656c56fd9 +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.remember/tmp/case-divergence +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.remember/tmp/last-ndc.ts +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.remember/tmp/last-save-ts +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.remember/tmp/last-save.json +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.remember/tmp/now-day +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.remember/tmp/post-tool-ran +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.remember/tmp/save-session.pid +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.remember/tmp/session-slug +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.remember/today-2026-08-10.md +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/CHANGELOG.md +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/MCP_SERVER.md +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/README.md +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/demo/linear.py +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/docs/01-goal-oriented-analysis.md +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/docs/03-plot-composition.md +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/docs/04-five-families.md +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/docs/05-distributions.md +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/docs/06-models-and-residuals.md +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/docs/07-visual-critique.md +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/docs/09-analysis-method.md +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/docs/10-teaching-path.md +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/docs/README.md +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/goad_mcp.py +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/img/distribution_fit.png +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/img/goaded.png +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/img/linear_results.png +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/img/residuals.png +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/img/zscores.png +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/src/goad_toolkit/__init__.py +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/src/goad_toolkit/analytics.py +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/src/goad_toolkit/cli.py +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/src/goad_toolkit/config.py +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/src/goad_toolkit/dataprocessor.py +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/src/goad_toolkit/distributions.py +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/src/goad_toolkit/filehandler.py +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/src/goad_toolkit/models.py +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/src/goad_toolkit/visualizer.py +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/tests/test_cli.py +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/tests/test_distributions.py +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/tests/test_filehandler.py +0 -0
- {goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/tests/test_visualizer.py +0 -0
|
@@ -59,6 +59,7 @@ input frame is safe, and the steps stay cheap.
|
|
|
59
59
|
| `RollingAvg` | rolling mean, drops the leading NaNs | `column`, `window`, `rename` |
|
|
60
60
|
| `ZScaler` | standardise to mean 0, std 1 | `column`, `rename` |
|
|
61
61
|
| `TimeFeatures` | derive calendar columns from a timestamp | `column`, `features` |
|
|
62
|
+
| `RegexFeature` | count / flag / extract a pattern in a text column | `column`, `pattern`, `feature`, `mode` |
|
|
62
63
|
|
|
63
64
|
`rename=True` writes to a new column (`deaths_shifted`, `deaths_zscore`, …) instead of
|
|
64
65
|
overwriting. Prefer it. An overwritten column is a step you cannot debug, and the whole
|
|
@@ -104,39 +105,39 @@ Two rules for your `transform`:
|
|
|
104
105
|
- **Name your parameters explicitly** in the signature. `def transform(self, data, column,
|
|
105
106
|
window)` documents itself; `**kwargs` does not, and the validation cannot help you.
|
|
106
107
|
|
|
107
|
-
## 2.5
|
|
108
|
+
## 2.5 `RegexFeature`: feature enrichment from text
|
|
108
109
|
|
|
109
|
-
The single most useful
|
|
110
|
+
The single most useful transform for text data, and the reason `TransformBase` is worth
|
|
111
|
+
subclassing at all. It ships:
|
|
110
112
|
|
|
111
113
|
```python
|
|
112
|
-
|
|
113
|
-
"""Add a feature extracted from a text column with a regular expression."""
|
|
114
|
-
|
|
115
|
-
def transform(
|
|
116
|
-
self,
|
|
117
|
-
data: pd.DataFrame,
|
|
118
|
-
column: str,
|
|
119
|
-
pattern: str,
|
|
120
|
-
feature: str,
|
|
121
|
-
mode: str = "count",
|
|
122
|
-
) -> pd.DataFrame:
|
|
123
|
-
text = data[column].fillna("")
|
|
124
|
-
if mode == "count":
|
|
125
|
-
data[feature] = text.str.count(pattern)
|
|
126
|
-
elif mode == "has":
|
|
127
|
-
data[feature] = text.str.contains(pattern, regex=True)
|
|
128
|
-
elif mode == "extract":
|
|
129
|
-
data[feature] = text.str.extract(pattern, expand=False)
|
|
130
|
-
else:
|
|
131
|
-
raise ValueError(f"mode must be count/has/extract, got {mode!r}")
|
|
132
|
-
return data
|
|
133
|
-
```
|
|
114
|
+
from goad_toolkit.datatransforms import RegexFeature
|
|
134
115
|
|
|
135
|
-
```python
|
|
136
116
|
pipeline.add(RegexFeature, name="url_flag",
|
|
137
117
|
column="message", pattern=r"https?://\S+", feature="has_url", mode="has")
|
|
138
118
|
```
|
|
139
119
|
|
|
120
|
+
Three modes, each writing one new column named by `feature`:
|
|
121
|
+
|
|
122
|
+
| `mode` | writes | use for |
|
|
123
|
+
|---|---|---|
|
|
124
|
+
| `"count"` | how many times the pattern occurs (int) | how many URLs, how many question marks |
|
|
125
|
+
| `"has"` | whether it occurs at all (bool) | flags you will group or filter on |
|
|
126
|
+
| `"extract"` | the first capture group, NaN where nothing matched | pulling a value *out* of the text |
|
|
127
|
+
|
|
128
|
+
`"extract"` needs exactly one capture group in `pattern`, and it is the mode worth being
|
|
129
|
+
careful with. `count` and `has` fail visibly when a pattern is wrong — a column of all zeros
|
|
130
|
+
or all `False` is hard to miss. Extraction fails *silently*, filling with NaN, so it reports
|
|
131
|
+
its own coverage through `loguru`:
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
mentions: extracted 'addressed_to' from 92,415/627,172 rows (14.7%); 534,757 rows had no match
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
That number is the point. A pattern that matches 15% of rows may be exactly right — on IRC,
|
|
138
|
+
most messages do not address anyone — or it may be silently broken. The log line makes you
|
|
139
|
+
decide which, instead of finding out four notebooks later.
|
|
140
|
+
|
|
140
141
|
Note the two different `name`s: `Pipeline.add(name=...)` names the *step*, and
|
|
141
142
|
`TransformBase.__init__` consumes it. So the new-column parameter has to be called something
|
|
142
143
|
else — `feature` here. Any transform that wants to name an output column hits this.
|
|
@@ -84,6 +84,7 @@ class TransformBase(ABC):
|
|
|
84
84
|
| `RollingAvg` | `column: str, window: int, rename: bool = False` |
|
|
85
85
|
| `ZScaler` | `column: str, rename: bool = False` |
|
|
86
86
|
| `TimeFeatures` | `column: str, features: Optional[List[str]] = None` |
|
|
87
|
+
| `RegexFeature` | `column: str, pattern: str, feature: str, mode: str = "count"` |
|
|
87
88
|
|
|
88
89
|
`rename=True` writes to `{column}_shifted` / `_diff` / `_rolling_avg` / `_zscore` instead of
|
|
89
90
|
overwriting.
|
|
@@ -92,6 +93,12 @@ overwriting.
|
|
|
92
93
|
`TimeFeatures.DEFAULT` — used when `features` is omitted — is `("day_name", "isoweek",
|
|
93
94
|
"year_week")`. A feature named the same as `column` overwrites it.
|
|
94
95
|
|
|
96
|
+
`RegexFeature.mode` is one of `"count"` (int), `"has"` (bool) or `"extract"` (the first
|
|
97
|
+
capture group, NaN where nothing matched); anything else raises `ValueError`. `extract`
|
|
98
|
+
requires exactly one capture group in `pattern` and logs its match rate through `loguru`.
|
|
99
|
+
The output column is `feature`, not `name` — `Pipeline.add(name=...)` already claims `name`
|
|
100
|
+
for the step.
|
|
101
|
+
|
|
95
102
|
```python
|
|
96
103
|
class Pipeline:
|
|
97
104
|
def add(self, transform_class: Type[T], name: Optional[str] = None, **kwargs) -> "Pipeline"
|
|
@@ -2,6 +2,7 @@ from abc import ABC, abstractmethod
|
|
|
2
2
|
from typing import Any, Dict, List, Optional, Type, TypeVar
|
|
3
3
|
|
|
4
4
|
import pandas as pd
|
|
5
|
+
from loguru import logger
|
|
5
6
|
from tqdm import tqdm
|
|
6
7
|
|
|
7
8
|
T = TypeVar("T", bound="TransformBase")
|
|
@@ -161,6 +162,52 @@ class TimeFeatures(TransformBase):
|
|
|
161
162
|
return data
|
|
162
163
|
|
|
163
164
|
|
|
165
|
+
class RegexFeature(TransformBase):
|
|
166
|
+
"""Add a feature extracted from a text column with a regular expression.
|
|
167
|
+
|
|
168
|
+
Three modes, all writing a single new column named by `feature`:
|
|
169
|
+
|
|
170
|
+
- `"count"` — how many times the pattern occurs per row (int);
|
|
171
|
+
- `"has"` — whether it occurs at all (bool);
|
|
172
|
+
- `"extract"` — the first capture group's text, or NaN where nothing matched.
|
|
173
|
+
`pattern` must contain exactly one capture group in this mode.
|
|
174
|
+
|
|
175
|
+
The new column is `feature`, not `name`: `Pipeline.add(name=...)` already claims
|
|
176
|
+
`name` for the step, and `TransformBase.__init__` consumes it. Any transform that
|
|
177
|
+
names an output column has to spell that parameter something else.
|
|
178
|
+
|
|
179
|
+
`mode="extract"` reports its own coverage through `loguru`. `count` and `has` fail
|
|
180
|
+
visibly when a pattern is wrong — a column of all zeros or all False. Extraction
|
|
181
|
+
fails silently, filling with NaN, so it says out loud how much it matched.
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
def transform(
|
|
185
|
+
self,
|
|
186
|
+
data: pd.DataFrame,
|
|
187
|
+
column: str,
|
|
188
|
+
pattern: str,
|
|
189
|
+
feature: str,
|
|
190
|
+
mode: str = "count",
|
|
191
|
+
) -> pd.DataFrame:
|
|
192
|
+
text = data[column].fillna("")
|
|
193
|
+
if mode == "count":
|
|
194
|
+
data[feature] = text.str.count(pattern)
|
|
195
|
+
elif mode == "has":
|
|
196
|
+
data[feature] = text.str.contains(pattern, regex=True)
|
|
197
|
+
elif mode == "extract":
|
|
198
|
+
data[feature] = text.str.extract(pattern, expand=False)
|
|
199
|
+
matched = data[feature].notna().sum()
|
|
200
|
+
total = len(data)
|
|
201
|
+
share = f"{matched / total:.1%}" if total else "n/a"
|
|
202
|
+
logger.info(
|
|
203
|
+
f"{self.name}: extracted '{feature}' from {matched:,}/{total:,} rows "
|
|
204
|
+
f"({share}); {total - matched:,} rows had no match"
|
|
205
|
+
)
|
|
206
|
+
else:
|
|
207
|
+
raise ValueError(f"mode must be count/has/extract, got {mode!r}")
|
|
208
|
+
return data
|
|
209
|
+
|
|
210
|
+
|
|
164
211
|
class Pipeline:
|
|
165
212
|
"""Pipeline for chaining data transformations."""
|
|
166
213
|
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
import pytest
|
|
3
|
+
|
|
4
|
+
from goad_toolkit.datatransforms import Pipeline, RegexFeature, 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
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@pytest.fixture
|
|
70
|
+
def messages() -> pd.DataFrame:
|
|
71
|
+
return pd.DataFrame(
|
|
72
|
+
{
|
|
73
|
+
"message": [
|
|
74
|
+
"daftykins: try rebooting?",
|
|
75
|
+
"see https://example.com and https://other.example",
|
|
76
|
+
"no url, no mention, no question",
|
|
77
|
+
None,
|
|
78
|
+
]
|
|
79
|
+
}
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_count_mode(messages):
|
|
84
|
+
result = RegexFeature(
|
|
85
|
+
column="message", pattern=r"https?://\S+", feature="n_url", mode="count"
|
|
86
|
+
)(messages.copy())
|
|
87
|
+
|
|
88
|
+
assert result["n_url"].tolist() == [0, 2, 0, 0]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_has_mode(messages):
|
|
92
|
+
result = RegexFeature(
|
|
93
|
+
column="message", pattern=r"\?", feature="has_question", mode="has"
|
|
94
|
+
)(messages.copy())
|
|
95
|
+
|
|
96
|
+
assert result["has_question"].tolist() == [True, False, False, False]
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_extract_mode_leaves_nan_where_nothing_matched(messages):
|
|
100
|
+
result = RegexFeature(
|
|
101
|
+
column="message",
|
|
102
|
+
pattern=r"^(\S+)[:,]\s",
|
|
103
|
+
feature="addressed_to",
|
|
104
|
+
mode="extract",
|
|
105
|
+
)(messages.copy())
|
|
106
|
+
|
|
107
|
+
assert result["addressed_to"].iloc[0] == "daftykins"
|
|
108
|
+
assert result["addressed_to"].isna().tolist() == [False, True, True, True]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_count_mode_defaults(messages):
|
|
112
|
+
# mode is the one parameter with a default; it counts
|
|
113
|
+
result = RegexFeature(column="message", pattern=r"\?", feature="n_question")(
|
|
114
|
+
messages.copy()
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
assert result["n_question"].tolist() == [1, 0, 0, 0]
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_na_rows_are_treated_as_empty_text(messages):
|
|
121
|
+
result = RegexFeature(
|
|
122
|
+
column="message", pattern=r"\?", feature="has_question", mode="has"
|
|
123
|
+
)(messages.copy())
|
|
124
|
+
|
|
125
|
+
# the None row must not propagate NaN into a boolean column
|
|
126
|
+
assert result["has_question"].dtype == bool
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def test_unknown_mode_raises(messages):
|
|
130
|
+
with pytest.raises(ValueError, match="mode must be count/has/extract"):
|
|
131
|
+
RegexFeature(column="message", pattern=r"\?", feature="x", mode="extractt")(
|
|
132
|
+
messages.copy()
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def test_missing_column_raises(messages):
|
|
137
|
+
with pytest.raises(ValueError, match="does not exist"):
|
|
138
|
+
RegexFeature(column="nope", pattern=r"\?", feature="x")(messages.copy())
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def test_extract_on_an_empty_frame_does_not_divide_by_zero():
|
|
142
|
+
empty = pd.DataFrame({"message": pd.Series([], dtype="object")})
|
|
143
|
+
|
|
144
|
+
result = RegexFeature(
|
|
145
|
+
column="message", pattern=r"^(\S+):", feature="addressed_to", mode="extract"
|
|
146
|
+
)(empty)
|
|
147
|
+
|
|
148
|
+
assert len(result) == 0
|
|
149
|
+
assert "addressed_to" in result.columns
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def test_feature_is_the_column_name_and_name_is_the_step(messages):
|
|
153
|
+
pipeline = Pipeline()
|
|
154
|
+
pipeline.add(
|
|
155
|
+
RegexFeature,
|
|
156
|
+
name="mentions",
|
|
157
|
+
column="message",
|
|
158
|
+
pattern=r"^(\S+)[:,]\s",
|
|
159
|
+
feature="addressed_to",
|
|
160
|
+
mode="extract",
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
result = pipeline.apply(messages)
|
|
164
|
+
|
|
165
|
+
assert "addressed_to" in result.columns
|
|
166
|
+
assert "mentions" not in result.columns
|
|
167
|
+
assert "mentions" in repr(pipeline)
|
|
@@ -1,66 +0,0 @@
|
|
|
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
|
|
File without changes
|
{goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.claude/worktrees/quizzical-solomon-b62511/.gitignore
RENAMED
|
File without changes
|
{goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.claude/worktrees/quizzical-solomon-b62511/.python-version
RENAMED
|
File without changes
|
{goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.claude/worktrees/quizzical-solomon-b62511/CHANGELOG.md
RENAMED
|
File without changes
|
{goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.claude/worktrees/quizzical-solomon-b62511/MCP_SERVER.md
RENAMED
|
File without changes
|
{goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.claude/worktrees/quizzical-solomon-b62511/README.md
RENAMED
|
File without changes
|
{goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.claude/worktrees/quizzical-solomon-b62511/demo/linear.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.claude/worktrees/quizzical-solomon-b62511/docs/README.md
RENAMED
|
File without changes
|
{goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.claude/worktrees/quizzical-solomon-b62511/goad_mcp.py
RENAMED
|
File without changes
|
|
File without changes
|
{goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.claude/worktrees/quizzical-solomon-b62511/img/goaded.png
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.claude/worktrees/quizzical-solomon-b62511/img/zscores.png
RENAMED
|
File without changes
|
{goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.claude/worktrees/quizzical-solomon-b62511/pyproject.toml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{goad_toolkit-0.2.4 → goad_toolkit-0.2.5}/.claude/worktrees/quizzical-solomon-b62511/uv.lock
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|