Exploratory Data Analysis skill for Codex
Findings with evidence, not forty histograms. Codex starts EDA from the questions and the grain of the data, checks quality before trusting any number, and works through distributions, relationships and segments with sample sizes beside every rate. Metric moves are decomposed into volume, rate and mix before anyone explains them, surprising numbers are derived twice, and the result is a short findings report with caveats.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- exploratory-data-analysis Installs to ~/.agents/skills/exploratory-data-analysis/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install exploratory-data-analysis The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project exploratory-data-analysis Installs to .agents/skills/exploratory-data-analysis/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/exploratory-data-analysis.zipunzip -o exploratory-data-analysis.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) exploratory-data-analysis PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $exploratory-data-analysis, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Runs a disciplined exploratory data analysis on a new dataset in pandas, Polars, DuckDB or SQL - questions first, then structure, data quality, univariate distributions, relationships and segments - and reports findings with evidence, caveats and next steps instead of a dump of charts. Use when someone hands over a dataset and asks what is in it, wants to understand drivers of a metric, needs a sanity check before modelling or a dashboard, or asks why a number moved.
Use it when
- A new dataset arrives and someone asks what is in it
- A key metric moved and the cause is unknown
- Data needs a sanity check before modelling, a dashboard or a decision
- Stakeholders want a written summary of patterns with evidence
Use something else when
- The data first needs heavy fixing of types, duplicates and formats: use data-cleaning
- You are evaluating a randomised experiment: use ab-test-analysis
- The data is a time-indexed series needing seasonality or anomaly work: use time-series-analysis
How to check the result
Headline totals reconcile to a known source figure, each finding cites the query or cell that produced it, and a clean rerun reproduces every number.
The mistake it prevents
Reporting a rate for a segment without its sample size, or explaining a drop that is really a mix shift between segments.
Good EDA ends with a short list of findings that each carry a number, a chart or table that shows it, and a statement of how sure you are. It answers the questions that were asked, flags what makes the data untrustworthy, and separates observation from explanation. Forty unlabelled histograms are not EDA.
Before touching the data
- Write down the questions, in the user’s words. If none were given, ask what decision the analysis supports; propose 3 candidate questions if they are unsure.
- Find out what one row represents (the grain), the time range, and how the data was produced (event log, survey, export from a CRM). Collection method explains most anomalies.
- Read
AGENTS.mdfor the stack and where outputs go (reports/, notebooks, a doc). Pick one engine: pandas or Polars for files under a few GB, DuckDB for larger files or SQL-minded users, warehouse SQL when the data lives there. - Work on a copy or read-only connection; EDA never writes back to the source.
Workflow
- [ ] 1. Shape: rows, columns, grain, time span, key uniqueness
- [ ] 2. Quality: nulls, sentinels, duplicates, impossible values, freshness gaps
- [ ] 3. Univariate: distribution of each important column
- [ ] 4. Target / metric: its level, trend and distribution
- [ ] 5. Relationships: metric vs candidate drivers, pairwise
- [ ] 6. Segments: where the metric differs most, with sample sizes
- [ ] 7. Checks: re-derive any surprising number a second way
- [ ] 8. Write findings, caveats and next steps
Time-box each step. If quality problems dominate (step 2), stop and report them; analysis on broken data produces confident nonsense.
Step 1-2: shape and quality
import pandas as pd
df = pd.read_parquet("data/orders.parquet")
print(df.shape)
print(df.dtypes.value_counts())
print(df["order_id"].is_unique, df["created_at"].min(), df["created_at"].max())
quality = pd.DataFrame({
"dtype": df.dtypes.astype(str),
"null_pct": df.isna().mean().mul(100).round(1),
"n_unique": df.nunique(),
"top": df.apply(lambda s: s.value_counts(dropna=True).index[:1].tolist()),
}).sort_values("null_pct", ascending=False)
print(quality.to_string())
# volume over time: gaps and spikes are usually pipeline events, not business events
daily = df.set_index("created_at").resample("D").size()
print(daily.describe()); print(daily[daily == 0].index[:10])
-- DuckDB equivalent for files too large for memory
summarize select * from 'data/orders/*.parquet';
select date_trunc('day', created_at) d, count(*) from 'data/orders/*.parquet' group by 1 order by 1;
Look for: constant columns, columns with one value per row (IDs), suspicious round-number
spikes, default dates (1970-01-01, 1900-01-01), and days with zero rows.
Step 3-4: distributions
| Column kind | Show | Watch for |
|---|---|---|
| Continuous (amount, duration) | quantiles p1, p5, p50, p95, p99, max; histogram on log scale if skewed | Heavy tails, spikes at caps, negative values |
| Count (items, sessions) | value counts for small values, quantiles above | Excess zeros |
| Categorical | top 15 categories with share, count of the rest | Near-duplicate labels, long tail, “Other” dominating |
| Datetime | counts by day, weekday, hour (in the right time zone) | DST shifts, UTC vs local, batch-load timestamps |
| Boolean / flags | share true, by time | Flag introduced mid-period |
q = df["amount"].quantile([0.01, 0.05, 0.25, 0.5, 0.75, 0.95, 0.99]).round(2)
print(q, df["amount"].max())
print(df["channel"].value_counts(normalize=True).head(15).round(3))
Report medians and percentiles for skewed metrics; a mean of a revenue column is driven by a handful of customers.
Step 5-6: relationships and segments
- Continuous vs continuous: scatter (sample 50k points) and Spearman correlation, which tolerates outliers and monotone non-linear relations.
- Metric by category: table of count, mean or rate, and median per category. Always show the count next to the rate; a 40% conversion rate on 12 users is noise.
- Metric over time by segment: small multiples, one line per segment, shared y-axis.
- Two categoricals: crosstab with row percentages (
pd.crosstab(a, b, normalize="index")).
seg = (df.groupby("channel")
.agg(orders=("order_id", "size"), revenue=("amount", "sum"),
aov=("amount", "mean"), median_order=("amount", "median"))
.assign(share=lambda t: t["revenue"] / t["revenue"].sum())
.sort_values("revenue", ascending=False))
print(seg.round(2))
When a number moved (“revenue dropped 12% last week”), decompose before explaining: volume vs rate vs mix. Revenue = orders x average order value; check which factor changed, then which segment drove it. Mix shift (more traffic from a low-converting channel) explains many “drops” with no change inside any segment (Simpson’s paradox).
Charts
- One question per chart, with a title that states the finding (“Mobile drives 70% of the decline”), labelled axes with units, and the data source and date range in a caption.
- Save figures to files (
reports/figures/*.png) so the report is reproducible; Codex cannot see inline notebook output unless the notebook is executed and saved. - For styling and chart mechanics hand off to
$matplotlib-charts.
Rules of evidence
- Correlation found in EDA is a hypothesis. Phrase it as “is associated with” and propose the test or experiment that would confirm causation.
- Every surprising number gets a second derivation (different query, different tool, or a manual check of 5 raw rows) before it goes in the report.
- Say how many segments you looked at. Slicing 30 ways will find one “significant” gap by chance; treat unplanned segment findings as leads.
- Note what the data cannot answer (no cost data, no logged-out users, survivorship).
Gotchas
- Time zones: event times in UTC grouped by local day move late-evening activity to the
next day. Convert before
resampleordate_trunc. - Incomplete periods: the current week or month is partial; exclude it or label it.
- Filtering on an outcome (only completed orders) and then concluding about the whole population.
- Joins that change row counts. Check the count before and after every join.
- Sampling for speed is fine for plots, never for totals or rare-event rates.
Verification
- Totals in the report reconcile to a known figure (finance report, dashboard, source system count) within a stated tolerance, or the difference is explained.
- Each finding cites the query or cell that produced it.
- Rerunning the notebook or script from scratch reproduces every number in the report.
Report template
# EDA: <dataset> (<date range>)
**Questions:** 1) ... 2) ...
**Data:** 1.2M orders, one row per order, 2024-01-01 to 2025-09-30; source: orders export 2025-10-02
## Data quality
- 3.1% of orders have null channel (all before 2024-03-12, tracking launch)
- 2024-07-04 has zero rows: pipeline outage, excluded from trends
## Findings
1. **Repeat customers are 22% of buyers but 48% of revenue.** Median order 61 vs 38. (cell 14, fig 3)
2. **The September drop is mix, not conversion.** Paid social share rose 9 pts; conversion within each channel flat. (cell 19, fig 5)
## Caveats
- No refunds in this dataset; revenue is gross
- Segment findings beyond the two questions are exploratory (11 cuts examined)
## Next steps
- Confirm finding 2 with channel-level spend data
- A/B test the repeat-customer offer before attributing causality What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Exploratory Data Analysis"
short_description: "Findings with evidence, not forty histograms"
default_prompt: "$exploratory-data-analysis Here is our orders export - tell me what is interesting and what looks broken" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.