File size: 7,163 Bytes
8bf2593
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# P2 Feature & Target Engineering Tools

** Aditya Β· Sprint: June 1–12 Β· Status: shipped (registry 18 β†’ 22 tools in this snapshot)**

Four tools that close the gap between "I have a SQL result" and "I can
train an honest model on it": `derive_feature`, `define_target`,
`time_split`, `handle_imbalance`. Specs follow `docs/v1_tools.md` Β§5;
this doc is the implementation reference.

The canonical predictive chain is now:

```
run_sql(label="loans")
  β†’ derive_feature(df_label="loans", name="payment_burden",
                   expression="payments / amount")
  β†’ define_target(df_label="loans", target_column="y_default",
                  positive_definition="status = 'B'",
                  scope_filter="status IN ('A','B')",
                  entity_column="loan_id")
  β†’ time_split(df_label="loans", time_col="granted_date",
               cutoff="2017-06-01")
  β†’ handle_imbalance(df_label="train")          # defaults target from define_target
  β†’ train_tabular_model(df_label="train", target_column="y_default", ...)
  β†’ predict / evaluate_predictions on "test"
```

---

## 1. `derive_feature`

`lexsi_ds/agent/tools/derive_feature.py`

Adds ONE column to a cached `run_sql` DataFrame from a scalar
expression β€” a ratio, date diff, or CASE bucket β€” without a SQL
round-trip. Mutates `sql_result:<df_label>` in place (v1_tools Β§5.1
contract).

| Arg | Type | Default | Notes |
|---|---|---|---|
| `df_label` | `str` | β€” | cached frame to mutate |
| `name` | `str` | β€” | new column; identifier-validated |
| `expression` | `str` | β€” | scalar SQL (duckdb) or `df.eval` (pandas) |
| `engine` | `"duckdb" \| "pandas"` | `"duckdb"` | duckdb is binder-validated |
| `overwrite` | `bool` | `False` | guard against clobbering source columns |

Failure modes surfaced as `ok=False` with a recoverable observation:
`unknown_label` (lists cached labels), binder errors (lists available
columns), `column_exists`, `forbidden_expression` (statement-level SQL
keywords blocked), `non_rowwise_expression` (aggregates rejected β€”
those belong in `run_sql`).

## 2. `define_target`

`lexsi_ds/agent/tools/define_target.py`

Materializes the prediction target as an explicit column and records
the **recipe** β€” task type, positive definition, scope filter, entity
column β€” under `last_target_definition`. Fixes the v0 failure mode of
training on a raw multi-code column when the question was binary.

| Arg | Type | Default | Notes |
|---|---|---|---|
| `df_label` | `str` | β€” | |
| `target_column` | `str` | β€” | created if `positive_definition` given, else validated |
| `task_type` | `"classification" \| "regression"` | `"classification"` | |
| `positive_definition` | `str \| None` | `None` | SQL boolean (cls) / numeric expr (reg) |
| `scope_filter` | `str \| None` | `None` | rows kept for training; rest dropped |
| `entity_column` | `str \| None` | `None` | recorded so training excludes it |

Validation: β‰₯2 classes for classification (else `single_class_target`),
≀20 classes (else `high_cardinality_target` β€” likely a regression
target), numeric dtype for regression, NULL-target rows flagged.

## 3. `time_split`

`lexsi_ds/agent/tools/time_split.py`

Temporal train/test split: train = strictly before the cutoff, test =
at/after. Two modes β€” explicit `cutoff` (ISO date) or `test_fraction`
(chronologically-last fraction; the derived cutoff is reported).
VARCHAR date columns are coerced; >20% parse failures β†’
`bad_time_column`. A cutoff outside the data range β†’ `degenerate_split`
with the actual range in the observation so the planner can recover.
The observation states the no-overlap invariant explicitly ("every
train row precedes every test row") so the summariser can cite it.

Cache: `sql_result:<train_label>`, `sql_result:<test_label>`,
`last_split = {kind:"time", time_col, cutoff, train_label, test_label,
n_train, n_test, source_label}`. Sides under 50 rows are warned per the
spec.

## 4. `handle_imbalance`

`lexsi_ds/agent/tools/handle_imbalance.py`

Class-balance report + **ranked strategy proposal β€” it never resamples
itself** (the planner/user decides; silent data surgery is not
shippable in an enterprise deployment). Defaults its target from
`last_target_definition` so it chains naturally after `define_target`.

Severity bands on the imbalance ratio (majority/minority): balanced
(<1.5), mild (<3), moderate (<10), severe (β‰₯10). Strategy ranking is
data-aware: class weights always first; undersampling only when the
majority can spare rows (β‰₯2k); SMOTE/oversampling when the minority is
tiny (<200); **Lexsi synthetic augmentation offered only when
`ctx.org` is attached** (`train_synthetic_model` β†’
`generate_synthetic_data_points` on the platform), so offline runs stay
clean; metric guidance (AUROC/PR-AUC over accuracy) always appended for
moderate/severe. Continuous targets (>20 distinct values) are rejected
with `non_categorical_target` and a pointer at the regression path.

Cache: `last_imbalance_report`.

---

## 5. Feature lineage (enterprise upgrade, this iteration)

`lexsi_ds/agent/tools/_lineage.py`

All four tools append a structured record to
`ctx.cache["feature_lineage"]` (ordinal, tool, UTC timestamp,
operation details). This gives every trained model a reproducible
"how the training frame was built" recipe β€” the model-risk-management
audit artifact regulated customers ask for, and the data source for the
planned `export_report` (P3) audit section. Append-only, run-scoped,
zero hot-path cost.

## 6. New planner rules

Rules **20–23** added to `PLANNER_SYSTEM` (`lexsi_ds/agent/prompts.py`),
one per tool, additive over 1–19:

- **20** derive features with `derive_feature`, don't re-run SQL
- **21** time-split temporal data before training β€” never random-split it
- **22** `define_target` whenever the label must be constructed
- **23** `handle_imbalance` before classification training; cite AUROC/PR-AUC when moderate/severe

> Merge note: Bhavish's June-10 branch also claims rules 20–22 for his
> P1 tools (`detect_data_quality_issues`, `suggest_join_paths`,
> `sample_rows`). Merges shoud include second renumbers β€” the rules are
> independent prose blocks, so it's a mechanical shift.

## 7. Benchmark coverage

`bench/pkdd/p2_feature_engineering.yaml` β€” 10 items:

- happy + error path per tool (8), trajectory-scored: expected/forbidden
  tools, `ok` flag, error code, observation substrings, cache keys
  written;
- the **leakage pair** (2): the same default-risk question random-split
  vs time-split. Passes when the time-split run trains strictly
  pre-cutoff and reports an AUROC **lower** than the leaky random-split
  AUROC β€” demonstrating the leak rather than asserting a fixed number.
  This validates demo step 3.

## 8. Tests

`tests/test_p2_feature_engineering.py` β€” 25 hermetic tests (in-memory
DuckDB, no PKDD file, no SDK, no LLM), reusing the offline-synthetic
fixture family from `conftest.py` plus a local `temporal_loan_df`
fixture with a time-drifting label. Covers every happy/error path
above, the cache contracts, the no-temporal-overlap invariant, lineage
ordering, and registry wiring.