expdpy for AI agents and LLMs
expdpy ships first-class support for large language models and AI agents: a machine-readable map of the library, function-calling schemas, an MCP server, and a portable agent skill. This page is generated from the canonical skill, so it never drifts from what an agent receives.
Machine-readable entry points
/llms.txt- a curated index of the whole library (the llmstxt.org convention), with expdpy’s guardrails up front./llms-full.txt- every function, docstring and concept explainer concatenated into one file for full-context ingestion./tools/anthropic_tools.jsonand/tools/openai_tools.json- curated function-calling tool schemas for the Anthropic and OpenAI APIs.- Every docs page has a clean Markdown twin at
<page>.html.md.
MCP server
Install the optional extra and run the stdio server:
pip install "expdpy[mcp]"Register it with an MCP client (Claude Desktop, Claude Code, …):
{ "mcpServers": { "expdpy": { "command": "expdpy-mcp" } } }The server exposes a curated set of Explore/Analyze tools plus explain / list_topics / learn_concept. Each tool returns the result’s plain-language interpret() reading, its tables, and a saved figure path.
The use-expdpy agent skill
The skill below teaches an agent to use expdpy correctly. Copy the .claude/skills/use-expdpy/ directory into your project (Claude Code reads .claude/skills/), or paste the workflow below into your agent’s instructions.
use-expdpy
Use expdpy - Explore / Analyze / Learn for panel and cross-sectional data - to do real analysis: declare the panel, call one module-prefixed function, and read the frozen result it returns. Every function returns a typed result object with a uniform surface (.df, .fig, .gt) and, on most, plain-language .interpret() / .explain(). The value of this skill is doing the analysis the way the library intends - panel-aware, association-only, and self-documenting - instead of reaching for raw pandas/statsmodels.
expdpy is a Python port of the R package ExPanDaR. Import it as import expdpy as xp. The full, always-correct list of functions is in references/function-catalog.md (generated from the installed library, so it never lies). Read that before guessing a name.
The four-step workflow
Step 1 - Declare the panel once
Panel data has two index columns: an entity (the unit - country, region, firm) and a time column. Declare them once and every later call inherits them:
import expdpy as xp
from expdpy.data import load_kuznets, load_kuznets_data_def
df = xp.set_labels(load_kuznets(), load_kuznets_data_def(), set_panel=True)
# or, without a data dictionary:
df = xp.set_panel(load_kuznets(), entity="country", time="year")set_panel writes the panel onto df.attrs; resolve_panel reads it back. You may always override per call with the keyword-only entity= / time= arguments - explicit args win. The vocabulary is always entity and time - never id, unit, cs_id, or ts_id. If you are unsure which columns qualify, run python .claude/skills/use-expdpy/scripts/check_panel.py yourfile.csv for a read-only report of candidate index columns and dtypes.
Cross-sectional data (one row per entity, no time) needs no set_panel; pass it straight to the cross-sectional functions (e.g. analyze_iv_regression).
Step 2 - Pick the module
- Explore (
explore_*) - describe and visualize before modeling:explore_descriptive_table,explore_correlation_table,explore_missing_values_plot,explore_trend_plot,explore_scatter_plot,explore_spaghetti_plot,explore_xtsum_table,explore_panel_structure. Functions ending_plotreturn a Plotly.fig; functions ending_tablereturn a Great Tables.gt. - Analyze (
analyze_*) - estimate:analyze_regression_table/analyze_panel_table(OLS / fixed / random effects),analyze_iv_regression(2SLS),analyze_beta_convergence/analyze_sigma_convergence/analyze_convergence_clubs,analyze_kuznets_waves,analyze_event_study(staggered DiD). - Learn (
learn_*) - runnable concept sandboxes on simulated data (learn_omitted_variable_bias,learn_pooled_vs_fixed_effects, …) for teaching a concept, not for estimating on the user’s data.
When the intent is fuzzy, open references/choosing-a-function.md.
Step 3 - Read the result object (the universal contract)
Every explore_* / analyze_* / learn_* function returns a frozen result dataclass, never a bare figure or frame. The surface is uniform:
| Attribute | What it is |
|---|---|
.df |
the tidy underlying data / coefficient frame (a pandas.DataFrame) |
.fig |
an interactive Plotly figure (functions ending _plot) |
.gt |
a Great Tables object (functions ending _table) |
.interpret() |
one paragraph of plain-language, association-only interpretation |
.explain() |
the relevant concept explainer from the pedagogy registry |
.tidy() |
one row per coefficient/term (estimation results) |
.glance() |
one row of model-level scalars (N, R-squared, F, …) |
Not every method is meaningful on every result. Never call .show() on .fig in a script - to persist a figure, write it: result.fig.write_html("fig.html") (or .write_image("fig.png")). Full surface and idioms in references/result-contract.md.
Step 4 - Interpret responsibly (READ THIS BEFORE WRITING ANY CONCLUSION)
expdpy is deliberately built so the library never overclaims, and you must match it:
- Associations, not causation.
.interpret()describes how variables move together. It never contains the word “causes” or the phrase “effect of”, and it ends with a note pointing toxp.explain("correlation_vs_causation"). When you summarize a result, keep that framing: write “is associated with” / “is higher when”, not “causes” / “the effect of”. The one place a causal claim is legitimate is instrumental variables - and even there the causal argument is yours (you must defend the instrument’s relevance and exclusion), while.interpret()still reports the association. - Lead with
.interpret(). Prefer quoting the library’s own interpretation over inventing your own narrative; it is calibrated to the library’s standards. - Reach for
explain(topic)when a concept needs grounding.xp.list_topics()returns the concept keys (e.g.fixed_effects,omitted_variable_bias,instrumental_variables,beta_convergence).xp.explain("fixed_effects")returns anExplainerwithwhat/when_to_use/caveats/references. Use it to justify a method or warn about a pitfall, instead of writing econometrics from memory.
The full guardrail cheat-sheet is in references/guardrails.md.
Recipes (runnable, end-to-end, bundled data)
Recipe A - Descriptive + correlation overview of a panel
import expdpy as xp
from expdpy.data import load_kuznets, load_kuznets_data_def
df = xp.set_labels(load_kuznets(), load_kuznets_data_def(), set_panel=True)
desc = xp.explore_descriptive_table(df) # summarizes all numeric columns
corr = xp.explore_correlation_table(df[["gini_regional", "gdp_pc", "trade_share"]])
print(desc.df) # the numbers
desc.gt # the formatted Great Table (display in a notebook)
print(corr.interpret()) # association-only reading of the correlationsRecipe B - Instrumental variables (AJR 2001 colonial origins, cross-section)
import expdpy as xp
from expdpy.data import load_colonial_origins, load_colonial_origins_data_def
ajr = xp.set_labels(load_colonial_origins(), load_colonial_origins_data_def())
base = ajr[ajr["base_sample"] == 1] # the 64-country base sample
iv = xp.analyze_iv_regression(
base,
dv="log_gdp_pc_1995",
endog="expropriation_risk", # institutions: endogenous
instruments="log_settler_mortality", # the AJR instrument
)
print(iv.tidy()) # 2SLS coefficients (the instrumented slope ~ 0.94)
print(iv.glance()) # includes the first-stage weak-instrument F (watch for < 10)
print(iv.interpret()) # association framing; the causal case is YOURS to argueIV is the one method where a causal claim is on-topic - but only if you explicitly defend that log_settler_mortality is relevant (moves institutions) and excludable (affects income only through institutions). See xp.explain("instrumental_variables").
Recipe C - Kuznets waves across pooled / between / within estimators
import expdpy as xp
from expdpy.data import load_kuznets, load_kuznets_data_def
df = xp.set_labels(load_kuznets(), load_kuznets_data_def(), set_panel=True)
kw = xp.analyze_kuznets_waves(df, inequality="gini_regional", development="log_gdp_pc")
print(kw.interpret()) # how inequality co-moves with development - associational
kw.fig # pooled scatter with the fitted waveResources
references/function-catalog.md- every public function, grouped, with signature + one-line summary (generated from the installed library; always current).references/choosing-a-function.md- “I want to … -> call …” decision guide.references/guardrails.md- association-not-causation, entity/time vocabulary, reading.interpret(), and theexplain()registry.references/result-contract.md- the full.df/.fig/.gt/.interpret()/.explain()/.tidy()/.glance()surface and the figure-saving idiom.references/recipes.md- more end-to-end recipes (event-study/DiD, panel models + Hausman, robust inference, outlier treatment).scripts/check_panel.py- read-only helper that reports candidate entity/time columns.
Programmatic access (MCP server + tool schemas)
To let an agent call expdpy directly, install the MCP server with pip install 'expdpy[mcp]' and run expdpy-mcp (stdio). Static Anthropic/OpenAI function-calling schemas are published at https://cmg777.github.io/expdpy/tools/anthropic_tools.json and .../openai_tools.json, and a machine-readable map of the whole library is at https://cmg777.github.io/expdpy/llms.txt.
When to use a different skill
This skill is for using expdpy on data. To add or change a function in the library itself (write analyze_*/explore_*/learn_*, an estimator, a result dataclass, tests, wiring), use the write-function skill instead - it is the developer counterpart.