Spaces:
Sleeping
Sleeping
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.
|