Explore ML Data
Understand the dataset before designing a model. One project-level
EDA per workspace: an executable data/eda.py, a persisted
data/eda.md narrative, rich data/eda_<table>.html reports, and a
short JOURNAL section that links them. The findings feed the baseline
design note's learner / splitter / metric choices.
Next-step pointers — where you go after this skill
| You came here for… |
→ next |
| Bootstrap, before the first baseline |
→ back to iterate-ml-experiment § 0; the EDA findings inform the auto-drafted 01_baseline.md |
| User free-text ("explore the data") |
→ surface the findings; no further dispatch unless the user asks to model |
| Re-understand a changed data source |
→ re-run, overwrite data/eda.*, refresh the JOURNAL EDA section |
Always re-emit the Pre-flight checklist with evidence before
declaring the turn done.
Where this sits in the loop
EDA is a bootstrap-time gate (G-EDA) owned by this skill and
fired by iterate-ml-experiment § 0 before the baseline design
note. Ordering matters: the dataset facts (class balance, datetime /
group columns, missingness, cardinality) are exactly what justifies
the splitter (G-CV-SPLITTER), the metric default, and the learner
default. Running EDA after the model is designed defeats the purpose.
scaffold → JOURNAL → goal from data/README.md
│
└─► G-EDA (run | skip) ◄── this skill
│ run
└─► data/eda.py → execute → data/eda.md + HTML + JOURNAL §EDA
│
└─► auto-draft 01_baseline.md (cites the EDA findings)
Where things live — visual map
Two locations are kept separate: the raw data source (read-only,
may live anywhere) and the EDA deliverables (always under
<project>/data/).
| Path |
Durability |
Who writes it |
What it holds |
raw data source (data/, raw/, an absolute path, external) |
user-owned, READ-ONLY |
the user |
The dataset. EDA reads it; never modifies it. May be anywhere — not assumed to be data/ |
data/eda.py |
Durable (committed) |
This skill, once per workspace |
The jupytext # %% EDA cells. Source of truth. Openable as a notebook for the rich view |
data/eda.md |
Durable (committed) |
This skill (authored from the digest) |
The prose narrative: findings + modelling implications that the baseline note cites |
data/eda_<table>.html |
Durable (committed) |
data/eda.py via TableReport.write_html(...) |
The rich, interactive skrub report per table — for the human |
scratch/eda/eda.md |
Ephemeral (gitignored), optional |
run_cells.py when given a 2nd arg |
Per-cell digest the agent reads. Same content as stdout |
journal/JOURNAL.md § Data understanding (EDA) |
Durable (committed) |
This skill |
2–4 line summary + link to data/eda.md |
Mnemonic: the raw data is read-only and lives wherever the user
keeps it; data/eda.py is source; data/eda.md + the HTML are the
durable deliverables, always under data/; scratch/eda/ and
stdout are the ephemeral run digest.
Read-only-against-raw-data contract
The central rule. Surfaced as the first Stop condition below.
Allowed — this skill writes ONLY (deliverables always under
<project>/data/, created if absent):
data/eda.py — the EDA script (created / overwritten in place).
data/eda.md — the authored narrative.
data/eda_<table>.html — the skrub TableReport pages.
scratch/eda/ — the ephemeral digest.
journal/JOURNAL.md § Data understanding (EDA).
Forbidden:
- Modifying, deleting, renaming, re-encoding, or "cleaning" the
user's raw data files — wherever they live (
data/, another
folder, an absolute/external path). EDA reads them; it never
rewrites them. Data cleaning is the pipeline's job
(build-ml-pipeline), declared at fit time, not a one-off mutation.
- Writing anywhere outside the five paths above — no
src/<pkg>/
edits, no reports/ writes, no new experiment files.
- Designing the model: no
skore.evaluate(...), no project.put(...),
no learner selection here. EDA informs those; it does not make
them.
Stop conditions — read before anything else
- Read-only against the user's raw data. See § Read-only-
against-raw-data contract.
data/eda.py reads the raw files
(wherever they live) and writes only the data/eda.* deliverables.
- Deliverables always under
<project>/data/; the raw source is
separate. Write data/eda.py / data/eda.md /
data/eda_<table>.html under <project>/data/ (create the folder
if absent). The raw data the script reads may live anywhere
(data/, another in-repo folder, an absolute or external path) —
decouple the two: a RAW = <LOAD_RAW_DATA> source vs an EDA_DIR
output. Never assume the raw data is in data/.
- EDA precedes model design (G-EDA). In bootstrap, the gate fires
before
journal/01_baseline.md is drafted. It is binary:
run (place + execute data/eda.py, write the deliverables) or
skip (record Status: skipped — <date> in the JOURNAL section
and proceed). Do not silently bypass — fire the AskUserQuestion.
Free-text "go fast" / "quick baseline" does NOT resolve it.
- Agent feature required to execute. The cell runner needs
ipython. If it is missing and the user chose run, STOP and
delegate to python-env-manager § "Agent feature"
(G-AGENT-FEATURE). Do NOT type pixi add ... ipython yourself;
do NOT fabricate EDA output with hand-written print()s. If the
user declines the agent feature, fall back to the skip path
(record Status: skipped) — never loop between run and install.
- Symbol from memory is forbidden. Any
skrub / pandas /
polars symbol (TableReport, TableReport.json, write_html,
column_associations, the tabular reader, …) must come from
python-api this turn. Cache hits under
scratch/api/<lib>/<version>/ count; inline memory does not.
TableReport.json()'s key names are not formally documented and
drift across skrub versions — confirm them via python-api and
parse defensively (.get(...)).
- Library-agnostic — read facts off skrub, not pandas/polars. The
workspace may use pandas OR polars (G-TABULAR), whose summary
methods differ (
select_dtypes doesn't even exist in polars). The
structured facts come from skrub (TableReport(...).json(),
column_associations), which accept both. The ONLY library-
specific line is RAW = <LOAD_RAW_DATA>. Do not write
df.isna()/df.nunique()/df.select_dtypes(...) etc.
skrub.TableReport for dataframe overviews. Every table gets a
TableReport(RAW, title=..., verbose=0) written to
data/eda_<table>.html (the user-facing artifact) AND read via
.json() for the digest. verbose=0 keeps progress prints out of
the digest.
- Never end a cell on a bare
TableReport. Outside a notebook,
repr(TableReport(df)) is the useless <TableReport: use .open() to display>. Use report.write_html(...) (a statement) for the
HTML, and end cells on text-friendly expressions (RAW.shape,
a dict/list built from report.json(),
skrub.column_associations(RAW)) so the digest carries real
values. Mirrors audit's .frame() rule.
- Never gitignore the whole
data/; ask about the inputs. The
deliverables live in data/ and must stay committable, so the
whole data/ folder must never be in .gitignore. If the raw
inputs should be kept out of git (large / local-only), fire an
AskUserQuestion offering to ignore specific input patterns
(e.g. data/raw/, data/*.parquet) — default: don't. Then verify
the deliverables are tracked (git check-ignore data/eda.md must
return nothing). Never auto-edit .gitignore — that is
organize-ml-workspace's to write; surface the patch and ask.
- One project-level EDA. A single
data/eda.py covers the whole
dataset; multi-table data gets one TableReport cell per table
inside that one file (run the target/structure cells on the
target-bearing table). No eda_v2.py, no per-experiment EDA files,
not part of the four-way stem pairing. Re-understanding overwrites
data/eda.py in place.
- Don't design the model here. No splitter pick, no metric pick,
no learner pick. Record implications in
data/eda.md; the picks
happen in their owning gates (G-CV-SPLITTER, the baseline note).
- Harness "no clarifying questions" hints do NOT waive G-EDA or
G-AGENT-FEATURE. Both fire regardless.
- Post-hoc audit — required before ending the turn. Walk every
pre-flight row; surface unfilled Evidence cells explicitly.
Forbidden shortcuts
| Shortcut |
Why it's wrong |
| Design the baseline first, EDA "later if there's time" |
Inverts G-EDA. The point is to justify the modelling choices before making them. EDA runs first in bootstrap |
End a cell on a bare TableReport(df) to "show the report" |
Outside a notebook that repr is <TableReport: use .open() to display> — zero signal in the digest. Use write_html(...) + a text summary built from report.json() |
print(...) instead of a bare summary expression |
The runner captures bare last-expressions via result.result; print(...) lands in stdout and is harder to scan. Use bare expressions |
Use pandas/polars methods (df.isna(), df.nunique(), df.select_dtypes(...)) for the summaries |
Breaks on the other library (polars has no select_dtypes). Read the facts off skrub (TableReport(...).json(), column_associations) — agnostic to pandas/polars |
Clean / impute / drop columns in data/eda.py and re-save the raw file |
EDA is read-only against raw data. Cleaning belongs in the pipeline (build-ml-pipeline), applied at fit time for train/test consistency |
Assume the raw data is in data/ |
The raw source may live anywhere; only the deliverables are pinned to data/. Set RAW = <LOAD_RAW_DATA> to wherever the data actually is |
Gitignore the whole data/ folder |
The committed deliverables (data/eda.*) live there. Ignore only specific input patterns, and ask the user first |
| Run EDA without the agent feature by hand-writing the expected output |
Fabricated EDA is worse than none. Missing runner → G-AGENT-FEATURE (install) or the skip path |
pixi add ipython directly from this skill |
Install is owned by python-env-manager. This skill requests via G-AGENT-FEATURE |
Drop the authored data/eda.md and leave only the HTML |
The .md carries the modelling implications the baseline note cites and the JOURNAL section links. Both are required |
| Invent column meanings not visible in the data |
Report what the data shows. Domain semantics the user didn't state go in an explicit "open questions" list, not as asserted fact |
| Forget the JOURNAL § Data understanding update |
The section is the index entry; without it later sessions can't find the EDA. It is part of "done" |
Pre-flight — emit before any write or execution
Pre-flight (explore-ml-data):
- [ ] Trigger: bootstrap-G-EDA | user-request | data-changed
Evidence: caller + rule that matched
- [ ] Detection: EDA already present? data/eda.md + JOURNAL §EDA
Evidence: ls / Glob on data/eda.md + Read JOURNAL §EDA
| "n/a — first EDA"
- [ ] G-EDA resolved: run | skip
Evidence: AskUserQuestion id=<id>, answer=<run|skip>
| user free-text quote turn N
If skip: JOURNAL §EDA records "Status: skipped — <date>"; STOP here.
- [ ] Tabular library known (G-TABULAR): pandas | polars
Evidence: JOURNAL.md Status (Workspace decisions) | AskUserQuestion
via data-science-python-stack
- [ ] Raw data located (may be outside data/): <paths / loader>
Evidence: ls / Glob on the data location + the RAW load call placed
in data/eda.py | user-quoted path turn N
- [ ] data/ not gitignored as a whole; deliverables will be tracked
Evidence: `git check-ignore data/eda.md` returns nothing
| AskUserQuestion id=<id> on ignoring specific inputs
| "n/a — no .gitignore yet"
- [ ] Agent feature available (run path only):
`pixi run -e agent ipython -c "print(0)"` exit 0
Evidence: tool output | JOURNAL.md Status `agent feature: installed`
Missing → STOP, delegate to python-env-manager G-AGENT-FEATURE
(decline → fall back to skip path)
- [ ] python-api consulted for symbols used:
skrub.TableReport, TableReport.write_html, TableReport.json,
skrub.column_associations, the tabular reader (load cell only)
Evidence: Read/Write scratch/api/<lib>/<version>/<topic>.md (this turn)
| "n/a — cache hit + Read this turn"
- [ ] Template copy + substitution decided:
<pkg> → package name from src/<pkg>/
<LOAD_RAW_DATA> → the real loader, pointing wherever the data lives
<TARGET_COLUMN> → the target (from goal / data/README.md), or n/a
<table> → short slug per table for eda_<table>.html
Evidence: Read templates/eda.py this turn before Write data/eda.py
- [ ] Execution command shape confirmed:
pixi run -e agent python \
.agents/skills/audit-ml-pipeline/scripts/run_cells.py \
data/eda.py [scratch/eda/eda.md]
Evidence: command emitted before running
- [ ] Deliverables written: data/eda.md (prose + implications),
data/eda_<table>.html (≥1), JOURNAL §Data understanding
Evidence: Write of each | "n/a — skip path"
- [ ] Pre-flight re-emitted with evidence before final message.
Evidence: this checklist appears in the end-of-turn summary.
EDA file contract — overview
data/eda.py is jupytext percent format (# %%), executed by
the shared runner. Template: templates/eda.py. Full cell-by-cell
anatomy with right / wrong shapes: → references/cell_anatomy.md.
Substitutions
| Placeholder |
Replaced with |
<pkg> |
The importable package name (from src/<pkg>/); used for from <pkg> import PROJECT_ROOT (only to locate EDA_DIR = PROJECT_ROOT / "data") |
<LOAD_RAW_DATA> |
The real load of the raw file(s), pointing wherever the data lives (in data/, another folder, an absolute path, or external). Uses the workspace tabular lib (pandas/polars); skrub accepts both. The one library-specific line |
<TARGET_COLUMN> |
The target column name (from the goal / data/README.md), or remove the target cell if unsupervised / unknown |
<table> |
A short slug per table for the HTML filename (eda_<table>.html) — for a single table use the dataset name |
Cell sequence (what each cell does)
Brief outline; concrete examples → references/cell_anatomy.md.
- Module docstring (markdown) — what this file is, the
read-only-against-raw-data rule, raw-vs-deliverables split, how it
is executed.
- Imports + paths (code) —
import json, import skrub,
from <pkg> import PROJECT_ROOT, EDA_DIR = PROJECT_ROOT / "data"
(+ EDA_DIR.mkdir(parents=True, exist_ok=True)). No pandas/polars
import here.
- Load raw data (code, bare expression) —
RAW = <LOAD_RAW_DATA>
pointing wherever the data lives; end on RAW.shape.
- Per-table overview (code) —
report = skrub.TableReport(RAW, title=..., verbose=0); report.write_html(EDA_DIR / "eda_<table>.html"); then summary = json.loads(report.json())
and end on a dict/list of per-column dtype / null / cardinality
facts. One such cell per table.
- Target analysis (code, bare expression) — pick the target's
entry out of
summary["columns"]; it carries value counts
(classification) or a distribution summary (regression). Drives the
metric default and whether the splitter should stratify.
- Structure signals (code, bare expression) — datetime columns
(from skrub's inferred dtypes, catches string dates) and high
unique-ratio id/group columns. Drives the
G-CV-SPLITTER choice
(TimeSeriesSplit / GroupKFold).
- Associations (code, bare expression) —
skrub.column_associations(RAW) to flag strong predictors and
possible leakage.
- End (markdown) — reminder that the agent now authors
data/eda.md + the JOURNAL section from this digest.
write_html(...) is load-bearing on the overview cells (the human
artifact). verbose=0 and the bare report.json()-derived
expressions are load-bearing for a clean, library-agnostic digest.
For multi-table data, run cells 5–7 on the target-bearing table; for
very large data, load a row sample (see references/cell_anatomy.md).
Execution contract — one command
pixi run -e agent python \
.agents/skills/audit-ml-pipeline/scripts/run_cells.py \
data/eda.py
The runner (shared with audit-ml-pipeline) streams the digest to
stdout — the agent reads it directly from the bash tool output. Pass
a second arg scratch/eda/eda.md to also write the digest to a file.
For non-pixi workspaces, swap the activation prefix per
python-env-manager § "Agent feature".
This skill ships no runner of its own — there is no
explore-ml-data/scripts/. Always invoke the shared
audit-ml-pipeline/scripts/run_cells.py at the path above; don't
look for or fork a local copy.
Prerequisites for the run path: the workspace package must be
importable (from <pkg> import PROJECT_ROOT — editable install done
during scaffold) and skrub installed (Tier 1). If either import
fails, the digest shows the ImportError; route to
python-env-manager for the missing piece rather than working around
it.
Re-execution semantics
- A changed / added data source → overwrite
data/eda.py, re-run,
re-author data/eda.md + HTML, refresh the JOURNAL section.
scratch/eda/ is overwritten on every run. The durable record is
data/eda.py + data/eda.md + git history.
Authoring data/eda.md
After the run, read the digest and write data/eda.md from
templates/eda.md. It is prose, grounded in the digest — no invented
facts. Required sections:
- Dataset at a glance — tables, rows × columns, target.
- Per-column findings — dtypes, missingness, cardinality
highlights, anything surprising.
- Target — balance / skew; class counts or distribution summary.
- Structure — datetime ordering, groups / ids (or "none found").
- Associations — strong feature↔target / feature↔feature links;
flag possible leakage explicitly.
- Modelling implications — the payoff section. Translate findings
into candidate picks the baseline note will weigh: e.g.
"imbalanced target →
StratifiedKFold + look at ROC-AUC / PR-AUC,
not accuracy"; "user_id repeats across rows → consider
GroupKFold"; "timestamp present → TimeSeriesSplit if forecasting".
These are implications, not decisions — the gates own the picks.
- Open questions — domain ambiguities for the user to confirm.
Link each data/eda_<table>.html from the relevant section.
JOURNAL § Data understanding (EDA)
iterate-ml-experiment's JOURNAL.md carries a top-level
## Data understanding (EDA) section (placed right after ## Status). This skill owns its content:
## Data understanding (EDA)
- **Status:** done — <YYYY-MM-DD> <!-- or: skipped — <YYYY-MM-DD> -->
- **Summary:** <2–4 lines: dataset shape, target balance/skew, the
one or two findings that most shape the modelling choices>
- **Report:** [data/eda.md](../data/eda.md)
Keep it to a few lines — it is an index entry, not the report. The
detail lives in data/eda.md. On the skip path, only the
Status: skipped line is required.
Dispatching in and out
Called from
| Caller |
When |
iterate-ml-experiment § 0 bootstrap |
Automatic; G-EDA fires before the baseline design note |
| User free-text |
"explore the data", "do an EDA", "profile the dataset" — resolves directly |
Calls into
| Callee |
Why |
python-env-manager § Agent feature |
When ipython is missing on the run path — G-AGENT-FEATURE |
python-api |
Every skrub / pandas / polars symbol. Cache hits first |
data-science-python-stack |
G-TABULAR (pandas / polars) if not yet recorded; skrub TableReport reference |
python-code-style |
After writing data/eda.py — ruff format / check + contextualize the comments to this dataset (strip any leftover workflow/process prose) |
What this skill does NOT do
- Design, select, or evaluate a model (
build-ml-pipeline /
evaluate-ml-pipeline / iterate-ml-experiment).
- Pick the CV splitter or metric — it only surfaces the evidence
for those picks.
- Edit
src/<pkg>/ or the experiment / audit files.
- Clean, transform, or re-save the user's raw data.
- Install
ipython / pyright (python-env-manager owns).
- Open or write the skore Project.
- Render commits or PRs.
Companion skills
| Skill |
Relationship |
iterate-ml-experiment |
Caller. § 0 fires G-EDA before the baseline note; the EDA findings seed the note's Method / Risks |
audit-ml-pipeline |
Owns the shared cell runner scripts/run_cells.py this skill executes; same bare-expression discipline |
organize-ml-workspace |
Workspace layout; data/ is user-owned — this skill is the one exception that writes data/eda.* into it |
python-env-manager |
Agent feature install (G-AGENT-FEATURE). This skill requests; that skill installs |
python-api |
skrub / pandas / polars symbol lookups. Cache hits first |
data-science-python-stack |
G-TABULAR; skrub TableReport is catalogued there |
python-code-style |
ruff after writing data/eda.py |
Templates and assets
templates/eda.py — the data/eda.py skeleton. Copy + substitute;
don't rewrite from memory.
templates/eda.md — the data/eda.md report skeleton.
The cell runner is not owned here — it is
audit-ml-pipeline/scripts/run_cells.py (shared). Don't fork it.
References (load on demand)
references/cell_anatomy.md — concrete cell examples (right /
wrong shapes), the TableReport repr trap, the full cell
sequence, and how each finding maps to a downstream gate.
1---2name: explore-ml-data3description: Owns data understanding BEFORE any model is designed. Places and executes `data/eda.py` (a jupytext `# %%` script) via the shared in-process runner, reads the streamed digest, then writes a persisted `data/eda.md` report (plus linked `data/eda_<table>.html` skrub `TableReport` pages) and the `## Data understanding (EDA)` section of `journal/JOURNAL.md`. The point is to surface the dataset facts — shape, dtypes, missingness, cardinality, target balance / skew, datetime / group structure, feature associations — that JUSTIFY the later learner / splitter / metric decisions, so the user understands *why* the modelling choices are made. Uses `skrub.TableReport` for dataframe overviews and the shared runner `audit-ml-pipeline/scripts/run_cells.py`. Stops at "EDA executed, `data/eda.md` + HTML written, JOURNAL EDA section updated." Never designs the model, never edits `src/<pkg>/`, never modifies the user's raw data files. TRIGGER — any of: - `iterate-ml-experiment` § 0 bootstrap, BEFORE the baseline design note — th4---56# Explore ML Data78Understand the dataset before designing a model. One project-level9EDA per workspace: an executable `data/eda.py`, a persisted10`data/eda.md` narrative, rich `data/eda_<table>.html` reports, and a11short JOURNAL section that links them. The findings feed the baseline12design note's learner / splitter / metric choices.1314## Next-step pointers — where you go after this skill1516| You came here for… | → next |17|---|---|18| Bootstrap, before the first baseline | → back to `iterate-ml-experiment` § 0; the EDA findings inform the auto-drafted `01_baseline.md` |19| User free-text ("explore the data") | → surface the findings; no further dispatch unless the user asks to model |20| Re-understand a changed data source | → re-run, overwrite `data/eda.*`, refresh the JOURNAL EDA section |2122Always re-emit the Pre-flight checklist with evidence before23declaring the turn done.2425## Where this sits in the loop2627EDA is a **bootstrap-time gate (G-EDA)** owned by this skill and28fired by `iterate-ml-experiment` § 0 **before** the baseline design29note. Ordering matters: the dataset facts (class balance, datetime /30group columns, missingness, cardinality) are exactly what justifies31the splitter (`G-CV-SPLITTER`), the metric default, and the learner32default. Running EDA after the model is designed defeats the purpose.3334```35scaffold → JOURNAL → goal from data/README.md36 │37 └─► G-EDA (run | skip) ◄── this skill38 │ run39 └─► data/eda.py → execute → data/eda.md + HTML + JOURNAL §EDA40 │41 └─► auto-draft 01_baseline.md (cites the EDA findings)42```4344## Where things live — visual map4546Two locations are kept separate: the **raw data source** (read-only,47may live anywhere) and the **EDA deliverables** (always under48`<project>/data/`).4950| Path | Durability | Who writes it | What it holds |51|---|---|---|---|52| raw data source (`data/`, `raw/`, an absolute path, external) | user-owned, **READ-ONLY** | the user | The dataset. EDA reads it; never modifies it. May be anywhere — not assumed to be `data/` |53| `data/eda.py` | **Durable** (committed) | This skill, once per workspace | The jupytext `# %%` EDA cells. Source of truth. Openable as a notebook for the rich view |54| `data/eda.md` | **Durable** (committed) | This skill (authored from the digest) | The prose narrative: findings + **modelling implications** that the baseline note cites |55| `data/eda_<table>.html` | **Durable** (committed) | `data/eda.py` via `TableReport.write_html(...)` | The rich, interactive skrub report per table — for the human |56| `scratch/eda/eda.md` | Ephemeral (gitignored), optional | `run_cells.py` when given a 2nd arg | Per-cell digest the agent reads. Same content as stdout |57| `journal/JOURNAL.md` § Data understanding (EDA) | **Durable** (committed) | This skill | 2–4 line summary + link to `data/eda.md` |5859**Mnemonic:** the raw data is *read-only and lives wherever the user60keeps it*; `data/eda.py` is *source*; `data/eda.md` + the HTML are the61*durable deliverables, always under `data/`*; `scratch/eda/` and62stdout are the *ephemeral run digest*.6364## Read-only-against-raw-data contract6566The central rule. Surfaced as the first Stop condition below.6768**Allowed — this skill writes ONLY (deliverables always under69`<project>/data/`, created if absent):**7071- `data/eda.py` — the EDA script (created / overwritten in place).72- `data/eda.md` — the authored narrative.73- `data/eda_<table>.html` — the skrub `TableReport` pages.74- `scratch/eda/` — the ephemeral digest.75- `journal/JOURNAL.md` § Data understanding (EDA).7677**Forbidden:**7879- Modifying, deleting, renaming, re-encoding, or "cleaning" the80 user's raw data files — **wherever they live** (`data/`, another81 folder, an absolute/external path). EDA **reads** them; it never82 rewrites them. Data cleaning is the pipeline's job83 (`build-ml-pipeline`), declared at fit time, not a one-off mutation.84- Writing anywhere outside the five paths above — no `src/<pkg>/`85 edits, no `reports/` writes, no new experiment files.86- Designing the model: no `skore.evaluate(...)`, no `project.put(...)`,87 no learner selection here. EDA *informs* those; it does not make88 them.8990## Stop conditions — read before anything else9192- **Read-only against the user's raw data.** See § Read-only-93 against-raw-data contract. `data/eda.py` reads the raw files94 (wherever they live) and writes only the `data/eda.*` deliverables.95- **Deliverables always under `<project>/data/`; the raw source is96 separate.** Write `data/eda.py` / `data/eda.md` /97 `data/eda_<table>.html` under `<project>/data/` (create the folder98 if absent). The raw data the script *reads* may live anywhere99 (`data/`, another in-repo folder, an absolute or external path) —100 decouple the two: a `RAW = <LOAD_RAW_DATA>` source vs an `EDA_DIR`101 output. Never assume the raw data is in `data/`.102- **EDA precedes model design (G-EDA).** In bootstrap, the gate fires103 **before** `journal/01_baseline.md` is drafted. It is binary:104 **run** (place + execute `data/eda.py`, write the deliverables) or105 **skip** (record `Status: skipped — <date>` in the JOURNAL section106 and proceed). Do not silently bypass — fire the `AskUserQuestion`.107 Free-text "go fast" / "quick baseline" does NOT resolve it.108- **Agent feature required to execute.** The cell runner needs109 `ipython`. If it is missing and the user chose **run**, STOP and110 delegate to `python-env-manager` § "Agent feature"111 (`G-AGENT-FEATURE`). Do NOT type `pixi add ... ipython` yourself;112 do NOT fabricate EDA output with hand-written `print()`s. If the113 user declines the agent feature, **fall back to the skip path**114 (record `Status: skipped`) — never loop between run and install.115- **Symbol from memory is forbidden.** Any `skrub` / `pandas` /116 `polars` symbol (`TableReport`, `TableReport.json`, `write_html`,117 `column_associations`, the tabular reader, …) must come from118 `python-api` *this turn*. Cache hits under119 `scratch/api/<lib>/<version>/` count; inline memory does not.120 **`TableReport.json()`'s key names are not formally documented and121 drift across skrub versions — confirm them via `python-api` and122 parse defensively (`.get(...)`).**123- **Library-agnostic — read facts off skrub, not pandas/polars.** The124 workspace may use pandas OR polars (G-TABULAR), whose summary125 methods differ (`select_dtypes` doesn't even exist in polars). The126 structured facts come from `skrub` (`TableReport(...).json()`,127 `column_associations`), which accept both. The ONLY library-128 specific line is `RAW = <LOAD_RAW_DATA>`. Do not write129 `df.isna()`/`df.nunique()`/`df.select_dtypes(...)` etc.130- **`skrub.TableReport` for dataframe overviews.** Every table gets a131 `TableReport(RAW, title=..., verbose=0)` written to132 `data/eda_<table>.html` (the user-facing artifact) AND read via133 `.json()` for the digest. `verbose=0` keeps progress prints out of134 the digest.135- **Never end a cell on a bare `TableReport`.** Outside a notebook,136 `repr(TableReport(df))` is the useless `<TableReport: use .open()137 to display>`. Use `report.write_html(...)` (a statement) for the138 HTML, and end cells on **text-friendly** expressions (`RAW.shape`,139 a `dict`/`list` built from `report.json()`,140 `skrub.column_associations(RAW)`) so the digest carries real141 values. Mirrors audit's `.frame()` rule.142- **Never gitignore the whole `data/`; ask about the inputs.** The143 deliverables live in `data/` and must stay committable, so the144 whole `data/` folder must never be in `.gitignore`. If the raw145 inputs should be kept out of git (large / local-only), fire an146 `AskUserQuestion` offering to ignore **specific input patterns**147 (e.g. `data/raw/`, `data/*.parquet`) — default: don't. Then verify148 the deliverables are tracked (`git check-ignore data/eda.md` must149 return nothing). Never auto-edit `.gitignore` — that is150 `organize-ml-workspace`'s to write; surface the patch and ask.151- **One project-level EDA.** A single `data/eda.py` covers the whole152 dataset; multi-table data gets one `TableReport` cell per table153 inside that one file (run the target/structure cells on the154 target-bearing table). No `eda_v2.py`, no per-experiment EDA files,155 not part of the four-way stem pairing. Re-understanding overwrites156 `data/eda.py` in place.157- **Don't design the model here.** No splitter pick, no metric pick,158 no learner pick. Record *implications* in `data/eda.md`; the picks159 happen in their owning gates (`G-CV-SPLITTER`, the baseline note).160- **Harness "no clarifying questions" hints do NOT waive G-EDA or161 G-AGENT-FEATURE.** Both fire regardless.162- **Post-hoc audit — required before ending the turn.** Walk every163 pre-flight row; surface unfilled Evidence cells explicitly.164165## Forbidden shortcuts166167| Shortcut | Why it's wrong |168|---|---|169| Design the baseline first, EDA "later if there's time" | Inverts G-EDA. The point is to justify the modelling choices *before* making them. EDA runs first in bootstrap |170| End a cell on a bare `TableReport(df)` to "show the report" | Outside a notebook that repr is `<TableReport: use .open() to display>` — zero signal in the digest. Use `write_html(...)` + a text summary built from `report.json()` |171| `print(...)` instead of a bare summary expression | The runner captures bare last-expressions via `result.result`; `print(...)` lands in stdout and is harder to scan. Use bare expressions |172| Use pandas/polars methods (`df.isna()`, `df.nunique()`, `df.select_dtypes(...)`) for the summaries | Breaks on the other library (polars has no `select_dtypes`). Read the facts off `skrub` (`TableReport(...).json()`, `column_associations`) — agnostic to pandas/polars |173| Clean / impute / drop columns in `data/eda.py` and re-save the raw file | EDA is read-only against raw data. Cleaning belongs in the pipeline (`build-ml-pipeline`), applied at fit time for train/test consistency |174| Assume the raw data is in `data/` | The raw source may live anywhere; only the deliverables are pinned to `data/`. Set `RAW = <LOAD_RAW_DATA>` to wherever the data actually is |175| Gitignore the whole `data/` folder | The committed deliverables (`data/eda.*`) live there. Ignore only specific input patterns, and ask the user first |176| Run EDA without the agent feature by hand-writing the expected output | Fabricated EDA is worse than none. Missing runner → G-AGENT-FEATURE (install) or the skip path |177| `pixi add ipython` directly from this skill | Install is owned by `python-env-manager`. This skill *requests* via G-AGENT-FEATURE |178| Drop the authored `data/eda.md` and leave only the HTML | The `.md` carries the modelling implications the baseline note cites and the JOURNAL section links. Both are required |179| Invent column meanings not visible in the data | Report what the data shows. Domain semantics the user didn't state go in an explicit "open questions" list, not as asserted fact |180| Forget the JOURNAL § Data understanding update | The section is the index entry; without it later sessions can't find the EDA. It is part of "done" |181182## Pre-flight — emit before any write or execution183184```185Pre-flight (explore-ml-data):186- [ ] Trigger: bootstrap-G-EDA | user-request | data-changed187 Evidence: caller + rule that matched188- [ ] Detection: EDA already present? data/eda.md + JOURNAL §EDA189 Evidence: ls / Glob on data/eda.md + Read JOURNAL §EDA190 | "n/a — first EDA"191- [ ] G-EDA resolved: run | skip192 Evidence: AskUserQuestion id=<id>, answer=<run|skip>193 | user free-text quote turn N194 If skip: JOURNAL §EDA records "Status: skipped — <date>"; STOP here.195- [ ] Tabular library known (G-TABULAR): pandas | polars196 Evidence: JOURNAL.md Status (Workspace decisions) | AskUserQuestion197 via data-science-python-stack198- [ ] Raw data located (may be outside data/): <paths / loader>199 Evidence: ls / Glob on the data location + the RAW load call placed200 in data/eda.py | user-quoted path turn N201- [ ] data/ not gitignored as a whole; deliverables will be tracked202 Evidence: `git check-ignore data/eda.md` returns nothing203 | AskUserQuestion id=<id> on ignoring specific inputs204 | "n/a — no .gitignore yet"205- [ ] Agent feature available (run path only):206 `pixi run -e agent ipython -c "print(0)"` exit 0207 Evidence: tool output | JOURNAL.md Status `agent feature: installed`208 Missing → STOP, delegate to python-env-manager G-AGENT-FEATURE209 (decline → fall back to skip path)210- [ ] python-api consulted for symbols used:211 skrub.TableReport, TableReport.write_html, TableReport.json,212 skrub.column_associations, the tabular reader (load cell only)213 Evidence: Read/Write scratch/api/<lib>/<version>/<topic>.md (this turn)214 | "n/a — cache hit + Read this turn"215- [ ] Template copy + substitution decided:216 <pkg> → package name from src/<pkg>/217 <LOAD_RAW_DATA> → the real loader, pointing wherever the data lives218 <TARGET_COLUMN> → the target (from goal / data/README.md), or n/a219 <table> → short slug per table for eda_<table>.html220 Evidence: Read templates/eda.py this turn before Write data/eda.py221- [ ] Execution command shape confirmed:222 pixi run -e agent python \223 .agents/skills/audit-ml-pipeline/scripts/run_cells.py \224 data/eda.py [scratch/eda/eda.md]225 Evidence: command emitted before running226- [ ] Deliverables written: data/eda.md (prose + implications),227 data/eda_<table>.html (≥1), JOURNAL §Data understanding228 Evidence: Write of each | "n/a — skip path"229- [ ] Pre-flight re-emitted with evidence before final message.230 Evidence: this checklist appears in the end-of-turn summary.231```232233## EDA file contract — overview234235`data/eda.py` is **jupytext percent format** (`# %%`), executed by236the shared runner. Template: `templates/eda.py`. Full cell-by-cell237anatomy with right / wrong shapes: → `references/cell_anatomy.md`.238239### Substitutions240241| Placeholder | Replaced with |242|---|---|243| `<pkg>` | The importable package name (from `src/<pkg>/`); used for `from <pkg> import PROJECT_ROOT` (only to locate `EDA_DIR = PROJECT_ROOT / "data"`) |244| `<LOAD_RAW_DATA>` | The real load of the raw file(s), pointing wherever the data lives (in `data/`, another folder, an absolute path, or external). Uses the workspace tabular lib (pandas/polars); skrub accepts both. The one library-specific line |245| `<TARGET_COLUMN>` | The target column name (from the goal / `data/README.md`), or remove the target cell if unsupervised / unknown |246| `<table>` | A short slug per table for the HTML filename (`eda_<table>.html`) — for a single table use the dataset name |247248### Cell sequence (what each cell does)249250Brief outline; concrete examples → `references/cell_anatomy.md`.2512521. **Module docstring (markdown)** — what this file is, the253 read-only-against-raw-data rule, raw-vs-deliverables split, how it254 is executed.2552. **Imports + paths (code)** — `import json`, `import skrub`,256 `from <pkg> import PROJECT_ROOT`, `EDA_DIR = PROJECT_ROOT / "data"`257 (+ `EDA_DIR.mkdir(parents=True, exist_ok=True)`). No pandas/polars258 import here.2593. **Load raw data (code, bare expression)** — `RAW = <LOAD_RAW_DATA>`260 pointing wherever the data lives; end on `RAW.shape`.2614. **Per-table overview (code)** — `report = skrub.TableReport(RAW,262 title=..., verbose=0)`; `report.write_html(EDA_DIR /263 "eda_<table>.html")`; then `summary = json.loads(report.json())`264 and end on a `dict`/`list` of per-column dtype / null / cardinality265 facts. One such cell per table.2665. **Target analysis (code, bare expression)** — pick the target's267 entry out of `summary["columns"]`; it carries value counts268 (classification) or a distribution summary (regression). Drives the269 metric default and whether the splitter should stratify.2706. **Structure signals (code, bare expression)** — datetime columns271 (from skrub's inferred dtypes, catches string dates) and high272 unique-ratio id/group columns. Drives the `G-CV-SPLITTER` choice273 (`TimeSeriesSplit` / `GroupKFold`).2747. **Associations (code, bare expression)** —275 `skrub.column_associations(RAW)` to flag strong predictors and276 possible leakage.2778. **End (markdown)** — reminder that the agent now authors278 `data/eda.md` + the JOURNAL section from this digest.279280`write_html(...)` is load-bearing on the overview cells (the human281artifact). `verbose=0` and the bare `report.json()`-derived282expressions are load-bearing for a clean, library-agnostic digest.283For multi-table data, run cells 5–7 on the target-bearing table; for284very large data, load a row sample (see `references/cell_anatomy.md`).285286## Execution contract — one command287288```bash289pixi run -e agent python \290 .agents/skills/audit-ml-pipeline/scripts/run_cells.py \291 data/eda.py292```293294The runner (shared with `audit-ml-pipeline`) streams the digest to295stdout — the agent reads it directly from the bash tool output. Pass296a second arg `scratch/eda/eda.md` to also write the digest to a file.297For non-pixi workspaces, swap the activation prefix per298`python-env-manager` § "Agent feature".299300**This skill ships no runner of its own** — there is no301`explore-ml-data/scripts/`. Always invoke the shared302`audit-ml-pipeline/scripts/run_cells.py` at the path above; don't303look for or fork a local copy.304305**Prerequisites for the run path:** the workspace package must be306importable (`from <pkg> import PROJECT_ROOT` — editable install done307during scaffold) and `skrub` installed (Tier 1). If either import308fails, the digest shows the `ImportError`; route to309`python-env-manager` for the missing piece rather than working around310it.311312### Re-execution semantics313314- A changed / added data source → overwrite `data/eda.py`, re-run,315 re-author `data/eda.md` + HTML, refresh the JOURNAL section.316- `scratch/eda/` is overwritten on every run. The durable record is317 `data/eda.py` + `data/eda.md` + git history.318319## Authoring `data/eda.md`320321After the run, read the digest and write `data/eda.md` from322`templates/eda.md`. It is prose, grounded in the digest — no invented323facts. Required sections:324325- **Dataset at a glance** — tables, rows × columns, target.326- **Per-column findings** — dtypes, missingness, cardinality327 highlights, anything surprising.328- **Target** — balance / skew; class counts or distribution summary.329- **Structure** — datetime ordering, groups / ids (or "none found").330- **Associations** — strong feature↔target / feature↔feature links;331 flag possible leakage explicitly.332- **Modelling implications** — the payoff section. Translate findings333 into *candidate* picks the baseline note will weigh: e.g.334 "imbalanced target → `StratifiedKFold` + look at ROC-AUC / PR-AUC,335 not accuracy"; "`user_id` repeats across rows → consider336 `GroupKFold`"; "timestamp present → `TimeSeriesSplit` if forecasting".337 These are *implications*, not decisions — the gates own the picks.338- **Open questions** — domain ambiguities for the user to confirm.339340Link each `data/eda_<table>.html` from the relevant section.341342## JOURNAL § Data understanding (EDA)343344`iterate-ml-experiment`'s `JOURNAL.md` carries a top-level345`## Data understanding (EDA)` section (placed right after `##346Status`). This skill owns its content:347348```349## Data understanding (EDA)350351- **Status:** done — <YYYY-MM-DD> <!-- or: skipped — <YYYY-MM-DD> -->352- **Summary:** <2–4 lines: dataset shape, target balance/skew, the353 one or two findings that most shape the modelling choices>354- **Report:** [data/eda.md](../data/eda.md)355```356357Keep it to a few lines — it is an index entry, not the report. The358detail lives in `data/eda.md`. On the **skip** path, only the359`Status: skipped` line is required.360361## Dispatching in and out362363### Called from364365| Caller | When |366|---|---|367| `iterate-ml-experiment` § 0 bootstrap | Automatic; G-EDA fires **before** the baseline design note |368| User free-text | "explore the data", "do an EDA", "profile the dataset" — resolves directly |369370### Calls into371372| Callee | Why |373|---|---|374| `python-env-manager` § Agent feature | When `ipython` is missing on the run path — G-AGENT-FEATURE |375| `python-api` | Every skrub / pandas / polars symbol. Cache hits first |376| `data-science-python-stack` | G-TABULAR (pandas / polars) if not yet recorded; skrub `TableReport` reference |377| `python-code-style` | After writing `data/eda.py` — ruff format / check + contextualize the comments to this dataset (strip any leftover workflow/process prose) |378379## What this skill does NOT do380381- Design, select, or evaluate a model (`build-ml-pipeline` /382 `evaluate-ml-pipeline` / `iterate-ml-experiment`).383- Pick the CV splitter or metric — it only surfaces the *evidence*384 for those picks.385- Edit `src/<pkg>/` or the experiment / audit files.386- Clean, transform, or re-save the user's raw data.387- Install `ipython` / `pyright` (`python-env-manager` owns).388- Open or write the skore Project.389- Render commits or PRs.390391## Companion skills392393| Skill | Relationship |394|---|---|395| `iterate-ml-experiment` | Caller. § 0 fires G-EDA before the baseline note; the EDA findings seed the note's Method / Risks |396| `audit-ml-pipeline` | Owns the shared cell runner `scripts/run_cells.py` this skill executes; same bare-expression discipline |397| `organize-ml-workspace` | Workspace layout; `data/` is user-owned — this skill is the one exception that writes `data/eda.*` into it |398| `python-env-manager` | Agent feature install (G-AGENT-FEATURE). This skill requests; that skill installs |399| `python-api` | skrub / pandas / polars symbol lookups. Cache hits first |400| `data-science-python-stack` | G-TABULAR; skrub `TableReport` is catalogued there |401| `python-code-style` | ruff after writing `data/eda.py` |402403## Templates and assets404405- `templates/eda.py` — the `data/eda.py` skeleton. Copy + substitute;406 don't rewrite from memory.407- `templates/eda.md` — the `data/eda.md` report skeleton.408409The cell runner is **not** owned here — it is410`audit-ml-pipeline/scripts/run_cells.py` (shared). Don't fork it.411412## References (load on demand)413414- `references/cell_anatomy.md` — concrete cell examples (right /415 wrong shapes), the `TableReport` repr trap, the full cell416 sequence, and how each finding maps to a downstream gate.