Skip to main content
Glama

简体中文 | English

StatsPAI: an Agent&Python-native Stata/R replacement for applied causal inference

PyPI version Python versions License: MIT Tests Docs PyPI Downloads JOSS DOI

StatsPAI is for empirical researchers who would normally jump between Stata, R, and Python. Its goal is to make common Stata/R econometrics and causal-inference workflows feel native in Python: load a dataset, estimate a model, inspect diagnostics, export tables, and hand the result to an agent or notebook without leaving one API.

It is meant to be a practical replacement path for new Python-first work:

  • Stata-style routines: regress, ivregress, reghdfe, csdid, rdrobust, synth, psmatch2, esttab / outreg2.

  • R-style routines: lm, fixest, did, rdrobust, Synth, DoubleML, MatchIt, modelsummary, broom.

  • Stata conventions where they matter: vce="robust" / vce="cluster firm" on regress, ivreg and the likelihood estimators, Stata's small-sample factors and z / t reference distributions, test / lincom / margins, dydx() after any fit, and sp.stata("...", data=df) to run Stata command lines you already have.

  • Python-native outputs: .summary(), .tidy(), .plot(), .to_latex(), .to_docx(), .to_agent_summary() where supported by the result object.

  • Agent-native access: every public function is registered with a machine-readable schema (sp.list_functions(), sp.describe_function(), sp.function_schema()), and the bundled statspai-mcp server exposes the estimators to MCP clients such as Claude Code, Claude Desktop, and Cursor.

  • Companion Stata tooling: our own stata-code can work with StatsPAI so agents can understand existing Stata workflows, translate them into Python, and cross-check results more smoothly.

  • Companion skill repos: Auto-Empirical-Research-Skills, AER-Skills, Awesome-Journal-Skills, and Paper-WorkFlow can work alongside StatsPAI and an agent as the methods, journal, manuscript, and reproducibility skill layer.

StatsPAI is not a promise that every Stata/R command is bit-for-bit identical. The API is broad, and the numerical evidence behind it is uneven: some estimators are checked against R/Stata on identical data, others only against known-truth simulations, and many are API-stable without a numerical-parity claim yet. Every function carries a validation_status that says which case applies: validation_status distinguishes certified/validated evidence from API-stable breadth. See Validation before relying on a number for publication.


Install

pip install statspai

Python 3.10 – 3.13 on Linux and macOS. The core install covers estimation, diagnostics, the bundled datasets, .xlsx / .docx / LaTeX export, and matplotlib figures (.plot(), sp.ggdid() and the rest). seaborn / plotly styles and heavier backends are optional extras:

Extra

Adds

Needed for

statspai[plotting]

seaborn, plotly (matplotlib is a core dependency)

seaborn-styled and interactive plotly figures

statspai[fixest]

pyfixest (Python ≥ 3.10)

sp.fixest.* wrappers and the pyfixest cross-validation engine

statspai[bayes]

PyMC, ArviZ

Bayesian estimators

statspai[neural] / statspai[deepiv]

PyTorch

neural causal models, DeepIV

statspai[performance]

JAX

accelerated backends

statspai[rust] (until the next release: pip install statspai-hdfe)

statspai-hdfe

Rust kernels for HDFE demeaning, Poisson IRLS and cluster SEs in sp.fast.*, sp.hdfe_ols, sp.ppmlhdfe, sp.regress(cluster=); sp.fast.backend_info() reports whether they are used (guide)

statspai[spatial]

geopandas, libpysal, shapely

shapefile / geometry-based spatial weights

The interactive figure editor additionally needs ipywidgets inside Jupyter.

import statspai as sp

print(sp.datasets.list_datasets()[["name", "design", "source"]])

sp is the convention used throughout these docs, but the alias is your choice. If sp is already taken in your code (for example by import scipy as sp), import statspai as si works just as well.

StatsPAI bundles 14 datasets that load offline. Most are real published extracts (source == "bundled CSV"): Card (1995) NLSYM schooling data, LaLonde/NSW with a PSID comparison group, the U.S. Senate RD data distributed with R's rdrobust, California Proposition 99, the castle-doctrine panel, and NHEFS, among others. A few are deterministic simulated replicas calibrated to a published design (source == "simulated"), including the Callaway–Sant'Anna mpdta panel used below; their numbers are not the numbers from the original data.

At a glance: 1,680 registered functions across 92 submodules; 756k LOC (core) + 519k LOC (tests). Run python scripts/registry_stats.py to reproduce these numbers.


Related MCP server: JeffersonStats

If You Come From Stata Or R

What you used before

Stata / R examples

StatsPAI entry point

OLS / robust SE

reg y x, vce(robust) / lm() + sandwich

sp.regress("y ~ x", data=df, vce="robust")

Clustered SE

vce(cluster firm) / sandwich::vcovCL(), feols(..., cluster = ~firm)

vce="cluster firm" (or cluster="firm"); sp.feols(..., cluster="firm")

Logit / probit / count models

logit, probit, poisson, nbreg / glm(), MASS::glm.nb()

sp.logit(), sp.probit(), sp.poisson(), sp.nbreg(), sp.glm()

IV / 2SLS

ivregress 2sls / AER::ivreg()

sp.ivreg("y ~ (d ~ z) + x", data=df)

High-dimensional FE

reghdfe / fixest::feols()

sp.feols("y ~ x | firm + year", data=df)

Staggered DiD

csdid / did::att_gt()

sp.callaway_santanna() + sp.aggte()

Regression discontinuity

rdrobust / rdrobust::rdrobust()

sp.rdrobust()

Synthetic control

synth / Synth::synth()

sp.synth()

Matching / PSM

psmatch2 / MatchIt

sp.psmatch2(), sp.match()

Double machine learning

ddml / DoubleML

sp.dml()

Post-estimation

test, lincom, margins, dydx()

fit.test(), fit.lincom(), sp.margins(fit)

Publication tables

esttab, outreg2 / modelsummary

sp.regtable()

Run Stata lines as they are

a .do snippet

sp.stata("logit y x, vce(cluster id)\nmargins, dydx(x)", data=df)

Translate a command

—

sp.from_stata("reghdfe y x, absorb(id year)"), sp.from_r("feols(...)")

On regress, ivreg, glm, logit, probit, poisson, nbreg, the ordered / multinomial / conditional logits, the zero-inflated and hurdle models, liml and more, vce= / robust= follow Stata's vce() grammar (sp.feols keeps fixest's vcov= / cluster=): True, "robust", "vce(robust)", "oim", "hc0"–"hc3", and a cluster variable written inline ("cluster firm", "vce(cluster firm)", "cl firm"). A spelling an estimator does not implement raises an error instead of quietly falling back to different standard errors. Robust and cluster SEs apply Stata's small-sample factors, clustered OLS / IV use t(G-1), and likelihood-based fits report z. The oim / robust / cluster standard errors of 29 estimators are pinned to Stata 18 at the 1e-6 parity budget (three documented exceptions where the two optimisers stop at slightly different points). See the shared argument grammar guide.

Causal entry points also accept one shared set of argument names next to each estimator's native spelling: id= for the panel unit, time=, first_treat= for adoption cohorts, covariates=, and running= / cutoff= for RD. sp.callaway_santanna(data=mp, y="lemp", time="year", id="countyreal", first_treat="first_treat") is the same call as the t= / i= / g= version below; a misspelt keyword names the closest one (runing= → "did you mean 'running'?"), and sp.describe_function(name)["aliases"] lists the accepted spellings.

sp.esttab(), sp.outreg2(), and sp.modelsummary() still exist, but they are deprecated thin wrappers over sp.regtable() and emit a DeprecationWarning.


Compared With Other Python Packages

StatsPAI aims to be one broad Stata/R-style workbench. Several focused Python packages do one part of that job, often with a longer track record; if you only need that part, they are good choices.

Package

What it focuses on

How StatsPAI relates

pyfixest

fixest-style OLS / IV / GLM with high-dimensional fixed effects, event-study DiD, wild bootstrap, tables

StatsPAI has its own sp.feols (checked against R fixest) and uses pyfixest as an optional wrapper and cross-validation engine.

linearmodels

panel models, IV / GMM, system estimation

A core StatsPAI dependency for parts of the panel module, and an independent engine in sp.cross_validate.

DoubleML

double/debiased ML (PLR, PLIV, IRM, IIVM), with an R twin

sp.dml is checked against DoubleML on identical learners and folds.

EconML

heterogeneous treatment effects: DML, causal forests, DR learners, IV, policy learning

sp.metalearner is checked against EconML's S/T/X learners.

DoWhy

graph-based model → identify → estimate → refute workflow; graphical causal models

StatsPAI has DAG and causal-discovery tools, but is estimator-first rather than graph-first.

CausalPy

Bayesian-first quasi-experiments in PyMC (plus OLS via scikit-learn): DiD, synthetic control, RD, ITS, IV

StatsPAI centres frequentist econometric conventions (clustered / robust SEs, bias-corrected RD, CS-DiD aggregation) and cross-language parity evidence.

causallib

scikit-learn-style IPW, standardization, doubly robust estimation, and causal evaluation

StatsPAI covers these alongside regression, panel, DiD, RD, and synthetic-control workflows in one API.

Use StatsPAI when you want one package, one function registry, and one agent interface across the everyday Stata/R empirical workflow.


Beginner Examples With Results

The outputs below were produced with StatsPAI 1.29.0 on the bundled datasets and are pinned by tests/test_readme_examples.py, so they cannot drift from the code silently. Example 6 uses the Stata vce() grammar and post-estimation commands introduced in 1.29.0; on an older release install from source with pip install "statspai @ git+https://github.com/brycewang-stanford/StatsPAI". Long summaries are abridged (... marks omitted lines); the numbers are pinned by tests/test_readme_examples.py and tests/test_synth_placebo_pvalue.py, so they cannot silently drift from the code again.

1. OLS: the first regress / lm replacement

Question: how much higher is log wage for one more year of schooling in the Card (1995) NLSYM data?

import statspai as sp

card = sp.datasets.card_1995()
ols = sp.regress(
    "lwage ~ educ + exper + expersq + black + south + smsa",
    data=card,
    robust="hc1",
)
print(ols.summary())

Result:

Model: OLS
Method: Least Squares
Dependent Variable: lwage
...
           Coefficient  Std. Error  t-statistic  P>|t|  [0.025  0.975]
Intercept       4.7337      0.0702      67.4718 0.0000  4.5961  4.8712
educ            0.0740      0.0036      20.3208 0.0000  0.0669  0.0812
exper           0.0836      0.0067      12.4165 0.0000  0.0704  0.0968
expersq        -0.0022      0.0003      -7.0443 0.0000 -0.0029 -0.0016
black          -0.1896      0.0174     -10.8781 0.0000 -0.2238 -0.1555
south          -0.1249      0.0154      -8.1339 0.0000 -0.1550 -0.0948
smsa            0.1614      0.0152      10.6374 0.0000  0.1317  0.1912

Model Diagnostics:
--------------------
R-squared           : 0.2905
...

Read it like a Stata/R regression table: conditional on experience, race, region, and SMSA, one more year of schooling is associated with about 0.074 higher log wage (roughly 7.4%). This is a correlation, not yet a causal return: schooling is plausibly correlated with unobserved ability, which motivates the IV in example 2. The HC1 standard errors follow Stata's vce(robust) / sandwich::vcovHC(type = "HC1") convention.

2. IV / 2SLS: replace ivregress 2sls or AER::ivreg

Question: instrument schooling with growing up near a four-year college (nearc4).

import statspai as sp

card = sp.datasets.card_1995()
iv = sp.ivreg(
    "lwage ~ (educ ~ nearc4) + exper + expersq + black + south + smsa",
    data=card,
)
print(iv.summary())

Result:

Model: IV-2SLS
Method: Two-Stage Least Squares
Dependent Variable: lwage
...
           Coefficient  Std. Error  t-statistic  P>|t|  [0.025  0.975]
...
educ            0.1323      0.0492       2.6870 0.0072  0.0358  0.2288

Model Diagnostics:
...
First-stage F (educ)        : 16.7176
...
Partial R² (educ)           : 0.0055
Hausman F-stat              : 1.5390
Hausman p-value             : 0.2149

The IV estimate (0.132) is larger than OLS but about 13 times less precise. The instrument is not strong — nearc4 explains only 0.55% of the residual variation in schooling (first-stage F ≈ 16.7) — and the Hausman test does not reject exogeneity of educ (p = 0.21). Default standard errors are the unadjusted ones with the small-sample correction used by AER::ivreg (Stata: ivregress 2sls ..., small); pass robust="hc1" for heteroskedasticity-robust errors, and the first-stage F then uses the same variance estimator, as Stata's estat firststage does.

With a first stage this modest, report a weak-instrument-robust interval too:

ar = sp.anderson_rubin_ci(
    y="lwage", endog="educ", instruments=["nearc4"],
    exog=["exper", "expersq", "black", "south", "smsa"], data=card,
)
print(ar.summary())
Anderson-Rubin (AR) — weak-IV-robust confidence set
------------------------------------------------------------
  level                : 95%
  grid                 : 401 points on [-0.359, 0.624]
  confidence set       : [0.0384, 0.2612]

3. Staggered DiD: replace csdid or R did

Question: what is the average effect of minimum-wage increases on teen employment in the Callaway–Sant'Anna mpdta design?

import statspai as sp

mp = sp.datasets.mpdta()   # simulated replica of R did's mpdta
gt = sp.callaway_santanna(
    data=mp,
    y="lemp",
    t="year",
    i="countyreal",
    g="first_treat",
)
overall = sp.aggte(gt, type="simple", bstrap=False)
print(overall.summary())

Result:

==============================================================================
  Callaway and Sant'Anna (2021) — aggte[simple]
==============================================================================

  ATT:      -0.0330 ***
  Std. Error:  (0.0078)
  [95% CI]:    [-0.0482,  -0.0178]
  P-value:     <0.001
...
  Observations:    2,500
...

The aggregated ATT is about -0.033 log points and statistically precise. The bundled mpdta is a calibrated simulated replica, so this is not the number R reports on the original mpdta data. What is checked: the same CSV run through R did::att_gt() + aggte() and Stata csdid returns the same ATT and standard error (Track A parity module 04_csdid).

4. Regression discontinuity: replace rdrobust

Question: is there a party incumbency advantage at the zero-margin cutoff in U.S. Senate elections?

import statspai as sp

senate = sp.datasets.lee_2008_senate()  # rdrobust's Senate data: x = margin, y = vote share
rd = sp.rdrobust(data=senate, y="y", x="x", c=0)
print(rd.summary())

Result:

==============================================================================
  Sharp RD Estimation
==============================================================================

  RD Effect:       7.51 ***
  Std. Error:  (1.74)
  [95% CI]:    [4.09,  10.92]
  P-value:     <0.001

------------------------------------------------------------------------------
  Inference
------------------------------------------------------------------------------
      method  estimate     se      z  pvalue  ci_lower  ci_upper
Conventional    7.4141 1.4587 5.0826  0.0000    4.5551   10.2732
      Robust    7.5065 1.7413 4.3110  0.0000    4.0937   10.9193

------------------------------------------------------------------------------
  Observations:    1,297
...
  Bandwidth H:    17.7544
  Bandwidth B:    28.0281
...
  N Effective Left:    360
  N Effective Right:    323
...

The data are the extract of Cattaneo, Frandsen & Titiunik (2015, doi:10.1515/jci-2013-0010) that ships with R's rdrobust (the loader keeps its historical name): x is the party's vote-share margin in the election at time t and y its vote share (0–100) in the election at t+2, following rdrobust's own illustration. Barely winning at t raises the vote share at t+2 by about 7.4 percentage points (conventional), 7.5 with robust bias correction; the headline line reports the robust bias-corrected estimate and CI. On this data the default MSE-optimal bandwidths, estimates, and standard errors match R rdrobust::rdrobust() and Stata rdrobust (Track A parity module 06_rd). Observations are 1,297 because 93 rows have a missing outcome.

5. Synthetic control: replace Stata/R synth

Question: how did California's Proposition 99 affect cigarette sales?

import statspai as sp

prop99 = sp.datasets.california_prop99()
sc = sp.synth(
    data=prop99,
    outcome="cigsale",
    unit="state",
    time="year",
    treated_unit="California",
    treatment_time=1989,
)
print(sc.summary())

Result:

==============================================================================
  Synthetic Control Method
==============================================================================

  ATT:      -19.5 *
  Std. Error:  (10.8)
  [95% CI]:    [-58.6,  19.6]
  P-value:     0.077

------------------------------------------------------------------------------
  Detailed Estimates
------------------------------------------------------------------------------
         unit  weight
         Utah  0.3939
      Montana  0.2318
       Nevada  0.2049
  Connecticut  0.1091
New Hampshire  0.0454
     Colorado  0.0148
...

The estimate says California consumed about 20 fewer packs per capita per year after the intervention. The p-value is the in-space placebo rank: California's post/pre RMSPE ratio ranks 3rd of the 39 states, so p = 3/39 ≈ 0.077. This default matches on pre-treatment outcomes only; pass covariates= (e.g. ["lnincome", "retprice", "age15to24", "beer"]) for an ADH-style predictor specification. That path re-solves the nested V-W problem for every placebo state and is much slower: pass n_jobs=-1 to fit the placebos in parallel (bit-identical results), or placebo=False while iterating on the specification.

Read this number with its caveat, which the full summary also prints: classical SCM weights are often not uniquely identified on empirical data, and different correct solvers can land on different donor weights. StatsPAI's native solver is certified on uniquely identified designs and labelled identification-dependent elsewhere. On this specification R Synth reaches an ATT of about -19.59 rather than -19.51; pass backend="synth" (needs a local R with the Synth package; outcome-lag specification only) when you need R's exact numbers.

6. Logit, clustered SEs and post-estimation: Stata's logit + margins

Question: in Thornton's Malawi experiment, how much did a randomly offered cash incentive raise the probability that people collected their HIV test results? Villages are the clusters.

import statspai as sp

hiv = sp.datasets.thornton_hiv(complete_case=True)
fit = sp.logit("got ~ C(any) + distvct + male + age", data=hiv,
               vce="cluster villnum")
print(fit.summary())
print(sp.margins(fit, data=hiv, variables=["any"]).round(4))   # margins, dydx(any)

Result:

Model: Logit
Method: Maximum Likelihood (Newton-Raphson)
Dependent Variable: got
...
               Coefficient  Std. Error  z-statistic  P>|z|  [0.025  0.975]
Intercept          -0.6370      0.2010      -3.1691 0.0015 -1.0310 -0.2431
C(any)[T.1.0]       2.0178      0.0994      20.3029 0.0000  1.8230  2.2126
distvct            -0.1696      0.0408      -4.1610 0.0000 -0.2496 -0.0897
male               -0.0530      0.1051      -0.5046 0.6139 -0.2590  0.1529
age                 0.0100      0.0034       2.9046 0.0037  0.0032  0.0167
...
  variable  dy/dx      se        z  pvalue  ci_lower  ci_upper
0    1.any   0.45  0.0217  20.7367     0.0    0.4075    0.4926

As in Stata, the 9 rows with a missing age or village id leave the estimation sample (N = 2,825, 119 villages); the 4 dropped for a missing cluster variable are reported in a StatsPAIWarning and in fit.model_info["n_missing_cluster_dropped"].

The logit coefficient is on the log-odds scale; sp.margins reports the effect on the probability, with a delta-method standard error. any is a 0/1 treatment, so it enters as C(any) (Stata's i.any) and the margin is the discrete change from 0 to 1. Being offered any incentive raised the probability of collecting results by about 45 percentage points, close to the raw difference in means of 45.1 points. Without C() the column counts as continuous, as in Stata without i., and margins returns the average slope (0.3558). That slope is not the effect of the offer. The coefficients, the village-clustered standard errors, and the marginal effect match Stata 18 logit got i.any distvct male age, vce(cluster villnum) followed by margins, dydx(any) (0.4500223, SE 0.0217017).

Tests and linear combinations use the full covariance matrix and the fit's own reference distribution (χ² / z here, F / t after OLS):

fit.test("distvct = 0")    # chi2(1) = 17.31, p < 0.001  (Stata: test distvct)
fit.lincom("1.any + male") # 1.9648, SE 0.1412           (Stata: lincom 1.any + male)

If you already have the Stata lines, run them as they are:

ame = sp.stata("""
logit got i.any distvct male age, vce(cluster villnum)
margins, dydx(any)
""", data=hiv)

sp.stata returns the output of the last line — here the same marginal-effects table. A line it cannot translate faithfully (for example xtreg, fe without the panel id that xtset would have supplied) raises instead of running a different model; sp.from_stata(line) shows the Python call without running it.


Export Results

sp.regtable() is the single table builder behind every format. Build the table once, then write it wherever your co-authors need it:

import statspai as sp

card = sp.datasets.card_1995()
m1 = sp.regress("lwage ~ educ", data=card, robust="hc1")
m2 = sp.regress("lwage ~ educ + exper + expersq", data=card, robust="hc1")
m3 = sp.regress("lwage ~ educ + exper + expersq + black + south + smsa",
                data=card, robust="hc1")
m4 = sp.ivreg("lwage ~ (educ ~ nearc4) + exper + expersq + black + south + smsa",
              data=card, robust="hc1")

tbl = sp.regtable(
    m1, m2, m3, m4,
    model_labels=["OLS (1)", "OLS (2)", "OLS (3)", "2SLS (4)"],
    coef_labels={"educ": "Years of schooling", "exper": "Experience",
                 "expersq": "Experience squared", "black": "Black",
                 "south": "South", "smsa": "SMSA"},
    drop=["Intercept"],
    title="Returns to Schooling (Card 1995)",
    notes=["HC1 robust SE. Column (4) instruments schooling with nearc4."],
)
print(tbl)                    # terminal
tbl.to_excel("table1.xlsx")   # Excel
tbl.to_word("table1.docx")    # Word
tbl.to_latex()                # LaTeX source; also .to_markdown(), .to_html()

The images are the .xlsx files written by tbl.to_excel(), rendered with LibreOffice: the Card (1995) table above, and a LaLonde/NSW table regressing 1978 earnings on NSW treatment with a PSID comparison group. The LaLonde table is also a warning about observational comparisons: the treatment coefficient moves from -635 to +1,551 once pre-treatment earnings and demographics are controlled for. See the export guide for journal templates, standard-error formats, and single-model exports.


Interactive Plot Editing

If you miss Stata's Graph Editor, use sp.interactive(fig) on any matplotlib figure returned by StatsPAI. In Jupyter it opens an editing panel next to a live preview, so beginners can adjust a figure without learning every matplotlib option first. Requires pip install ipywidgets.

What it is for:

  • change titles, labels, fonts, colors, markers, line widths, grids, legends (including entry names), axis limits, figure size, and export DPI;

  • recolor a series together with its error bars and confidence band, or apply a palette such as okabe_ito (color-blind safe) or grayscale;

  • switch among StatsPAI's publication themes (academic, aea, minimal, cn_journal) and the built-in matplotlib and seaborn styles, for this figure only;

  • keep the data layer locked (protect_data=True by default): axis limits never hide a point, error bar or band, and data cannot be made invisible;

  • export reproducible Python code for the edits. Tests check, pixel for pixel, that the code reproduces the edited figure and that undo restores the original;

  • save journal-ready PDFs with embedded TrueType fonts, and run editor.check() for text that is too small or series that look alike in black and white.

Outside a notebook (scripts, servers, agents) pass the edits as a list: sp.interactive(fig, actions=[{"action": "apply_theme", "theme": "aea"}]). See the figure editor guide.

import statspai as sp

mp = sp.datasets.mpdta()
gt = sp.callaway_santanna(data=mp, y="lemp", t="year",
                          i="countyreal", g="first_treat")
agg = sp.aggte(gt, type="dynamic", bstrap=False)
fig, ax = sp.ggdid(agg)

editor = sp.interactive(fig)   # edit the plot in Jupyter
print(editor.generate_code())  # copy reproducible matplotlib edits

The screenshot above shows the intended workflow: preview on one side, editing controls on the other, and code export for reproducibility.


Everyday Workflow

import statspai as sp

card = sp.datasets.card_1995()
r1 = sp.regress(
    "lwage ~ educ + exper + expersq + black + south + smsa",
    data=card,
    robust="hc1",
)
r2 = sp.ivreg("lwage ~ (educ ~ nearc4) + exper + expersq + black + south + smsa", data=card)

print(r1.summary())                          # human-readable table
print(r1.tidy().head())                      # broom-style dataframe
print(r1.test("black = south"))              # Wald test, like Stata's `test`
print(r1.lincom("black - south"))            # like Stata's `lincom`
tbl = sp.regtable(r1, r2, model_labels=["OLS", "2SLS"])
tbl.to_word("table.docx")                    # Word table
tbl.to_excel("results.xlsx")                 # Excel table

Useful docs:


Using StatsPAI From An Agent

The same registry that powers sp.help() is exposed three ways.

In Python — discover functions and their schemas without reading source:

import statspai as sp

sp.list_functions(core=True)                   # the ~30 everyday verbs, in order
sp.list_functions(category="causal")[:5]      # names
sp.describe_function("rdrobust")               # parameters, aliases, validation
sp.function_schema("rdrobust")                 # JSON schema for tool calling
sp.from_stata("reghdfe y x, absorb(id year) vce(cluster id)")
# {'tool': 'feols', 'python_code': "sp.feols('y ~ x | id + year', data=df, cluster='id')", ...}
sp.stata("regress y x, vce(cluster id)", data=df)   # translate and run

From the shell — statspai list, statspai describe rdrobust, statspai search "synthetic control"; and estimation with the same payload an MCP client gets: statspai run callaway_santanna --data panel.csv --arg y=lemp --arg g=first_treat --arg t=year --arg i=countyreal, or the schema-driven shortcuts statspai did --data panel.csv --y lemp --treat treated --time year --id id. statspai route did --answer design=staggered routes a question to estimators without data; statspai mcp starts the server.

Over MCP — the package installs a statspai-mcp stdio server (pure Python, no extra dependencies). By default it lists the hand-curated estimators, workflow and pipeline tools plus three discovery meta-tools (search_functions → describe_function → call_function) that reach every registered function; statspai-mcp --profile full advertises every auto-generated tool as its own entry (several hundred, about 2 MB, more than a client context window holds; for schema dumps and debugging). Clients that connect to a URL use statspai-mcp --transport http (binds 127.0.0.1:8765/mcp by default; any other address needs STATSPAI_MCP_HTTP_TOKEN). It also ships workflow prompts (for example audit_did_result, stata_command_workflow) and resources such as statspai://catalog. Tools take a data_path (CSV, Stata .dta, and other formats pandas can read) and return structured JSON with data provenance, captured runtime warnings and any unsupported arguments. For Claude Code:

claude mcp add statspai -- statspai-mcp

For Claude Desktop, Cursor, and other clients:

{
  "mcpServers": {
    "statspai": { "command": "statspai-mcp", "args": [] }
  }
}

See the MCP workflow guide for data handoff, result handles, and the recommended detect → estimate → audit loop.

As a Claude Code skill — statspai skill install copies the packaged statspai-analysis skill (applied-econ / epidemiology / ML-causal pipelines with paper-ready exports) into ~/.claude/skills; statspai skill validate re-checks every API claim in it against the installed package, and with --run executes its pipelines on synthetic data. The installed skill is a copy and does not follow package upgrades: statspai skill status reports whether it is current, statspai skill install --force refreshes it. Agents using the library start at AGENTS.md and llms.txt.


Validation: What Has Been Checked, And What Has Not

StatsPAI has a large API surface, so validation status matters.

import statspai as sp

print(sp.describe_function("ivreg")["validation_status"])   # 'certified'
print(sp.list_functions(validation_status="certified")[:5])

Every registered function carries one of these tiers (counts at the last release; sp.list_functions(validation_status=...) gives the live numbers):

validation_status

Meaning

Functions

certified

compared with a named external reference implementation (R, Stata, or the method authors' Python package) on identical inputs, within a pre-registered tolerance

575

validated

known-truth simulation, published-number, coverage, or documented-convention evidence, but not in the main R/Stata harness

450

api_stable

stable public interface; unit tests exist, but no numerical-validation claim

651

experimental

method or API may still change

4

In other words, 1,025 of the 1,680 registered functions carry numerical evidence. Breadth is not the same as validation; check the tier of the functions you depend on.

The evidence scorecard puts the four checks a specialist makes on one page, each regenerated from committed artifacts: agreement with R / Stata on the same data, confidence interval coverage in Monte Carlo, speed against the reference on the same task, and the estimators that still have no evidence.

Cross-language parity, made queryable

The tiers above are derived from an auditable parity index: every verified function records what it was aligned against, to what tolerance, on which test, and how closely it matched. Each row traces to a committed test artifact (the pinned StatsPAI ↔ R ↔ Stata harness, version-locked via renv.lock + per-run provenance) — nothing is asserted from memory.

import statspai as sp

s = sp.parity_status("feols")
print(s)
# feols: bit-exact vs fixest::feols [py/R/Stata] (headline rel_est 5.2e-15 within rel_est<=1e-06, rel_se<=1e-06)
s["reference_versions"]          # {'R': 'R version 4.5.2 (2025-10-31)', 'fixest': '0.14.0'}

sp.parity_summary()              # coverage counts, including the unverified gap
sp.parity_matrix(status="bit-exact")

Grades: bit-exact (headline relative error ≤ 1e-6 against a named R/Stata reference), aligned (a documented, pre-registered looser tolerance), analytical-only (recovers a known DGP truth or closed-form identity), external-replication (reproduces published-paper numbers), and unverified (registered but no parity evidence attached yet — the honest gap). The full, auto-generated matrix is published at docs/parity.md.

For your own data, sp.cross_validate re-runs one estimand through every independent engine installed locally and reports whether they agree:

card = sp.datasets.card_1995()
cv = sp.cross_validate(card, "iv", y="lwage", endog=["educ"], instruments=["nearc4"],
                       covariates=["exper", "expersq", "black", "south", "smsa"])
print(cv.summary())
Engine              Estimate     Std.Err                95% CI    status
------------------------------------------------------------------------
statspai             0.13229     0.04923      [0.0358, 0.2288]        ok
pyfixest             0.13229     0.04923      [0.0358, 0.2288]        ok
linearmodels         0.13229     0.04923      [0.0358, 0.2288]        ok
R::fixest            0.13229     0.04923      [0.0358, 0.2288]        ok
------------------------------------------------------------------------
VERDICT: ✓ AGREE   (4/4 engines ran)

Engines that are not installed (pyfixest, or R with fixest) are skipped. Every engine is asked for the same variance estimator (including cluster= and vcov=) and the same small-sample convention, so the standard errors are compared as well as the point estimates.

Beyond point-parity, a Track-B coverage study runs B=1000 Monte Carlo replications per estimator and checks that 95% confidence intervals hit their nominal rate on known-truth DGPs, against a 99% Wilson acceptance band of [0.935, 0.967]. The thirteen materialized nominal rows (twelve known-truth DGPs) — OLS on an RCT (0.952), a 2×2 DiD (0.955), strong-instrument IV (0.962), Callaway–Sant'Anna staggered ATT (0.947), Sun–Abraham overall ATT (0.950), a two-way FE panel through sp.panel (0.948) and through sp.fast.feols (0.955), entropy balancing (0.945), a causal-forest AIPW ATE at 2,000 trees (0.959), DML IRM ATE (0.968), SDID with placebo SEs (0.928), sharp RD with the robust CI (0.934), and DML PLR with the default learners (0.883) — each also record bias, Monte Carlo SD and SE calibration. The SDID and RD shortfalls come with calibrated SEs (RD's intervals equal R rdrobust's draw by draw); the PLR shortfall is regularisation bias of the default gradient-boosting nuisances (0.95 with the true nuisances); see tests/coverage_monte_carlo/FINDINGS.md. The committed artifacts live under tests/coverage_monte_carlo/results_b1000/.


Life Cycle

StatsPAI is in active development (PyPI status "Beta") and uses semantic versioning. Releases add functions and optional parameters, but a stable function is never broken without notice. The old behaviour first warns (DeprecationWarning, or FutureWarning for a changing default) for at least one minor release, and every such change is listed in MIGRATION.md. A fix that changes returned numbers is flagged as a correctness fix there and in the changelog. The 32 core entry points (sp.list_functions(core=True)) have snapshot-tested signatures. A function marked experimental may change in any minor release and makes no numerical-validation claim; the tiers above say how far each function's numbers have been checked. The glossary defines these terms.


Changelog

Release notes live outside the README:

The README is intentionally focused on first-time users.


Paper

StatsPAI is described in a peer-reviewed paper in the Journal of Open Source Software (2026, 11(125), 10604): https://doi.org/10.21105/joss.10604. The reviewer-facing material prepared for that review remains the quickest way to audit the package:


Citation

If you use StatsPAI in research, cite the JOSS paper (preferred) and the underlying method papers for each estimator. sp.citation() returns the paper citation, sp.citation(which="software") the versioned software entry, and many result objects expose estimator-level citation helpers.

@article{wang2026statspaijoss,
  author  = {Wang, Biaoyue and Rozelle, Scott},
  title   = {StatsPAI: A Unified, Agent-Native Python Toolkit for
             Causal Inference and Applied Econometrics},
  journal = {Journal of Open Source Software},
  year    = {2026},
  volume  = {11},
  number  = {125},
  pages   = {10604},
  doi     = {10.21105/joss.10604},
  url     = {https://doi.org/10.21105/joss.10604}
}

@software{wang2026statspai,
  author  = {Wang, Biaoyue and Rozelle, Scott},
  title   = {StatsPAI: A Unified, Agent-Native Python Toolkit for
             Causal Inference and Applied Econometrics},
  year    = {2026},
  version = {1.29.0},
  doi     = {10.5281/zenodo.19933900},
  url     = {https://doi.org/10.5281/zenodo.19933900},
  license = {MIT}
}

License

MIT. See LICENSE.

Available Tools

45 tools
auditA
Read-only

Reviewer-grade audit on a result. Returns the literature checklist (parallel-trends test, honest-DID, Bacon decomposition, placebo, balance, ...) with status per item and the concrete suggest_function to call to fill any missing high-importance check.

Inputs: detail, data_id, data_csv, as_handle, +4 more. Returns: Fields: statspai://schema/result

audit_resultA
Read-only

Reviewer-grade audit of a fitted result (result_id from an as_handle=true call): every robustness check the literature expects for the design, with status 'passed|failed|missing|not_applicable' and a suggest_function for each missing one. not_applicable checks (an overid test on a just-identified IV) carry a 'reason' and are left out of n_total.

Inputs: result_id (required), detail, data_id, data_csv, +4 more. Returns: Fields: statspai://schema/result

bacon_decompositionA
Read-only

Goodman-Bacon (2021) decomposition: breaks the two-way fixed-effects DID estimator into its 2x2 comparison weights. Reveals whether treated-vs-treated comparisons (which can have negative weights) dominate the estimate. Run this before trusting a TWFE-DID point estimate. Validation: certified parity evidence.

Inputs: y (required), id (required), time (required), treat (required), +8 more. Returns: Fields: statspai://schema/result

bibtexA
Read-only

Return verified BibTeX entries from paper.bib (StatsPAI's single source of truth for citations). Pass one or more bib keys (e.g. 'callaway2021difference'). NEVER invent citations -- call this tool instead. Unknown keys return an empty entry plus a list of close matches.

Inputs: keys (required), detail, data_id, data_csv, +5 more. Returns: Fields: statspai://schema/result

briefB
Read-only

One-line brief of a fitted result (estimand, estimate, SE, CI, N). Same output as brief_result.

Inputs: detail, data_id, data_csv, as_handle, +4 more. Returns: Fields: statspai://schema/result

brief_resultA
Read-only

Return the one-line agent-friendly brief for a fitted result. Uses sp.brief(). Useful when an agent wants to summarise a chained workflow without paying for the full JSON payload again.

Inputs: result_id (required), detail, data_id, data_csv, +4 more. Returns: Fields: statspai://schema/result

callaway_santannaA
Read-only

Staggered DID (Callaway-Sant'Anna 2021): group-time ATT with doubly-robust, IPW, or regression-adjusted estimators. Robust to heterogeneous treatment effects where TWFE fails. Requires a cohort column g (first-treatment period; 0 = never-treated). Validation: certified evidence with scoped limitations. Known limitations: clustervars accepts a cluster that varies within unit only with panel=False (repeated cross-sections, as Stata csdid without ivar()); on a panel the influence functions are per unit.

Inputs: g (required), i (required), t (required), y (required), +10 more. Returns: Fields: statspai://schema/result

call_functionA
Read-only

Run ANY registered StatsPAI function by name with keyword arguments -- the escape hatch for the ~1,200 functions that are not individually listed as tools. arguments follows the parameter schema from describe_function; column names refer to the dataset passed as data_id / data_path. An argument the function does not accept refuses the call (error_kind unknown_argument, with did_you_mean). Supports as_handle / result_id chaining like every other tool.

Inputs: function (required), detail, data_id, data_csv, +8 more. Returns: Fields: statspai://schema/result

causalB
Read-only

End-to-end causal workflow: diagnose -> recommend estimator -> fit -> run robustness -> return result. The one-shot entry point that lets an agent analyse a dataset in a single call without orchestrating stages itself.

Inputs: y (required), id, time, cohort, +13 more. Returns: Fields: statspai://schema/result

check_identificationA
Read-only

Design-level identification diagnostics: bad controls, overlap, cohort sizes, IV first-stage F, clustering. Run BEFORE fitting any estimator to surface design problems.

Inputs: y (required), id, time, cohort, +14 more. Returns: Fields: statspai://schema/result

cross_validateA
Read-only

Cross-validate ONE estimand across INDEPENDENT engines (StatsPAI, pyfixest, linearmodels, DoubleML, R's fixest, Stata) and report whether they agree (AGREE / PARTIAL / DISAGREE / INSUFFICIENT). Use this to honour the cross-package reproducibility rule: trust a number only when >=2 independent implementations reproduce it. Needs a data_path.

Inputs: estimand (required), g, i, t, +17 more. Returns: Fields: statspai://schema/result

describe_dataA
Read-only

Profile a dataset (data_id, data_path or inline data): shape, dtypes, missing counts, head, numeric summary and -- for a handle -- the lineage of transforms that produced it.

Inputs: head, detail, data_id, data_csv, +7 more. Returns: Fields: statspai://schema/result

describe_functionA
Read-only

Full machine-readable metadata for one registered StatsPAI function: JSON-Schema parameters (types, defaults, enums), assumptions, pre-conditions, failure modes, alternatives, validation tier and evidence notes, plus the accepted keyword aliases. Read this before calling a function that is not in the listed tools.

Inputs: name (required), detail, data_id, data_csv, +5 more. Returns: Fields: statspai://schema/result

detect_designA
Read-only

Auto-detect the study design (panel / cross-section / RD / IV-style) from column shapes and types. Returns the guessed design plus the columns that drove the inference. Call this BEFORE recommend() when the user pastes a CSV with no context.

Inputs: detail, data_id, data_csv, as_handle, +6 more. Returns: Fields: statspai://schema/result

didA
Read-only

Difference-in-differences dispatcher. 2x2 (two periods, 0/1 treat) when id is omitted; with id, a 0/1 indicator on a multi-period panel is converted to first-treatment cohorts and Callaway-Sant'Anna runs. Pass method= to force '2x2', 'cs', 'sa', 'bjs', 'sdid' or 'ddd'. Validation: validated evidence tier (known-truth, reference, external-parity, or Monte Carlo artifact).

Inputs: y (required), time (required), treat (required), id, +15 more. Returns: Fields: statspai://schema/result

dmlA
Read-only

Double/debiased machine learning (Chernozhukov et al. 2018) with cross-fitting: partially linear (plr), interactive (irm, binary treatment), and the IV versions (pliv, iivm). Treatment effect under selection on observables with flexible nuisance models. Validation: certified evidence with scoped limitations. Known limitations: external_predictions, store_oof, and observation_ids are Python-only and absent from JSON/MCP inputs; external records accept no dict, JSON string, or file path; store_oof=True retains individual-level records for get_oof() and get_residuals() in memory only; ordinary serialization omits them; Retained IRM needs at least 10 rows per treatment arm in each training fold or raises DataInsufficient instead of fallback; internal explicit fold_indices require n_rep=1, while external repeated IRM records are allowed when the explicit partition is equivalent for every repeat.

Inputs: y (required), treat (required), covariates (required), model, +14 more. Returns: Fields: statspai://schema/result

ebalanceB
Read-only

Hainmueller (2012) entropy balancing. Targets the ATT by exactly balancing covariate means across treatment groups. No propensity-score model specification needed. Validation: certified parity evidence.

Inputs: y (required), treat (required), covariates (required), detail, +8 more. Returns: Fields: statspai://schema/result

feolsB
Read-only

OLS with high-dimensional fixed effects (R fixest / Stata reghdfe syntax): 'y ~ x1 + x2 | unit + year'. IV as 'y ~ x | fe | d ~ z'. vcov: 'iid', 'hetero', or {'CRV1': 'cluster_col'} (two-way: 'a + b'). Validation: certified parity evidence.

Inputs: fml (required), vcov, detail, cluster, +8 more. Returns: Fields: statspai://schema/result

from_rA
Read-only

Translate a single R / fixest / felm / did expression to a verified StatsPAI tool-call payload. Returns the same shape as from_stata. Supported callables: feols / felm / lm / att_gt / did. Pass ONE expression -- no assignment, no piping. Validation: validated evidence tier (known-truth, reference, external-parity, or Monte Carlo artifact).

Inputs: expression (required), detail, data_id, data_csv, +5 more. Returns: Fields: statspai://schema/result

from_stataA
Read-only

Translate a single Stata command to a verified StatsPAI tool-call payload. Returns python_code (string for chat replies) AND arguments (ready-to-dispatch JSON-RPC for tools/call). Tier-1 commands: regress / xtreg / reghdfe / ivreg2 / csdid / did_imputation / synth / rdrobust; count-panel commands include nbreg / xtnbreg / ppmlhdfe. Unrecognised commands return close-match suggestions instead of guessing. Validation: validated evidence tier (known-truth, reference, external-parity, or Monte Carlo artifact).

Inputs: command (required), detail, data_id, data_csv, +5 more. Returns: Fields: statspai://schema/result

honest_didA
Read-only

Rambachan-Roth (2023) 'honest' DID sensitivity analysis. Operates on a fitted event-study / DID result: pass result_id from a prior sp.event_study / sp.callaway_santanna / sp.did_imputation / sp.sun_abraham fit run with as_handle=true (or use the honest_did_from_result tool, which auto-extracts the event-study coefficients). Returns honest confidence intervals under varying degrees of parallel-trends violation (smoothness or relative-magnitude restrictions). Call this when a pre-trend test rejects at low power instead of abandoning the design. Validation: certified parity evidence.

Inputs: e, alpha, detail, m_grid, +8 more. Returns: Fields: statspai://schema/result

honest_did_from_resultB
Read-only

Rambachan-Roth (2023) honest CIs on a fitted DID / event-study result; betas and sigma are read off the result. Validation: validated evidence tier (known-truth, reference, external-parity, or Monte Carlo artifact).

Inputs: result_id (required), e, alpha, m_bar, +8 more. Returns: Fields: statspai://schema/result

interpret_resultA
Read-only

Natural-language interpretation of a fitted result (result_id): the estimate, its uncertainty and what the design does / does not identify, grounded in the result's own numbers. Uses the client's model via MCP sampling when advertised (no API key); otherwise returns a deterministic structured brief, never an invented narrative.

Inputs: result_id (required), detail, data_id, audience, +6 more. Returns: Fields: statspai://schema/result

ivA
Read-only

Instrumental-variables dispatcher: 2SLS, LIML, Fuller or GMM via method=, with first-stage, weak-IV (effective F) and endogeneity diagnostics. Formula 'y ~ x + (d ~ z1 + z2)'. Validation: certified parity evidence.

Inputs: formula (required), vce, detail, method, +7 more. Returns: Fields: statspai://schema/result

ivregA
Read-only

2SLS instrumental-variables regression with first-stage F diagnostics; classical SEs unless robust=. Formula syntax: 'y ~ x_exog + (d_endog ~ z_instrument)'. Validation: certified parity evidence.

Inputs: formula (required), detail, robust, data_id, +6 more. Returns: Fields: statspai://schema/result

load_dataA
Read-only

Load a dataset into the server and get a data_id handle. Source: data_path (file or URL), data_records (JSON rows) or data_csv (CSV text). Returns the handle plus shape, dtypes, missing counts, the first rows and a numeric summary. Pass data_id to any later tool instead of re-sending the file; chain transform_data to filter / reshape / winsorise / impute and get a new handle whose lineage is recorded in every result's data_provenance.

Inputs: name, detail, data_id, dataset, +8 more. Returns: Fields: statspai://schema/result

matchA
Read-only

Matching estimator dispatcher (ATT by default): nearest-neighbour on the propensity score or Mahalanobis distance, Abadie-Imbens nnmatch, CEM, optimal / full matching, kernel, radius, ... via method=. Returns balance diagnostics. Validation: certified evidence with scoped limitations. Known limitations: greedy nearest-neighbour matching without replacement is order-dependent: the m_order convention can differ across packages and materially moves the estimate (>5x spread on MatchIt::lalonde with Mahalanobis distance). m_order='data' and 'closest' reproduce MatchIt exactly; m_order='farthest' is StatsPAI's own dynamic rule and is not MatchIt-equivalent; bias_correction=True follows a different convention from Matching::Match's BiasAdjust: StatsPAI regresses on the full covariate vector with unweighted OLS over all controls, the reference regresses on the matching variables weighted by match counts, so bias-corrected estimates can differ from it by about 0.1%. The uncorrected estimate and its Abadie-Imbens standard error are exact.

Inputs: y (required), treat (required), covariates (required), detail, +9 more. Returns: Fields: statspai://schema/result

pipeline_didA
Read-only

End-to-end DID workflow: preflight -> did/CS estimator -> audit -> honest-DID sensitivity -> bacon decomposition -> brief. Returns one markdown report + the primary result_id. Use this when the user pastes a DID dataset and asks 'is the effect real?' -- the pipeline runs every diagnostic the literature expects.

Inputs: y (required), time (required), treat (required), id, +10 more. Returns: Fields: statspai://schema/result

pipeline_ivA
Read-only

End-to-end IV workflow: ivreg -> first-stage F (effective + Olea-Pflueger) -> Anderson-Rubin CI -> e-value. Returns one markdown report + result_id.

Inputs: formula (required), detail, data_id, data_csv, +5 more. Returns: Fields: statspai://schema/result

pipeline_rdA
Read-only

End-to-end RD workflow: rdrobust -> rdplot (PNG image) -> rddensity (McCrary) -> rdsensitivity (bandwidth). Returns one markdown report + result_id + an image content block.

Inputs: x (required), y (required), c, fuzzy, +8 more. Returns: Fields: statspai://schema/result

plot_from_resultA
Read-only

Render a fitted result's diagnostic plot (event study, rdplot, synth gap, balance, ROC) as a PNG image block. Optional edits restyle it with the data locked.

Inputs: result_id (required), kind, edits, detail, +7 more. Returns: Fields: statspai://schema/result

preflightA
Read-only

Run pre-fit identification checks for a chosen method on a DataFrame. Verdict in {PASS, WARN, FAIL}. ALWAYS call this before fitting on an unfamiliar dataset to surface design problems (overlap, cohort sizes, IV first-stage F, running-variable density at the cutoff).

Inputs: method (required), y, id, time, +13 more. Returns: Fields: statspai://schema/result

rdrobustA
Read-only

Sharp or fuzzy regression-discontinuity with robust bias-corrected CIs (Calonico-Cattaneo-Titiunik 2014). Use fuzzy= for IV-style fuzzy RD. Validation: certified evidence with scoped limitations. Known limitations: the weighted rbc bootstrap is not supported -- passing weights= with bootstrap='rbc' raises MethodIncompatibility; R-parity certification applies only to the configurations enumerated by sp.validation_scope(function='rdrobust') -- the default native mserd / triangular / p=1 / nn path (Track A 06), a manual bandwidth, and the weighted rows -- other selectors, kernels and vce choices are pinned for sp.rdbwselect or not at all for this entry point.

Inputs: x (required), y (required), c, fuzzy, +9 more. Returns: Fields: statspai://schema/result

recommendA
Read-only

Method advisor: given a dataset + research question, recommends a ranked list of estimators with reasoning, precondition checks, and a full suggested workflow. This is the first call an agent should make if it doesn't know which estimator to run. Supports DAG input, mediator / proxy / principal-strata variables, and optional resampling-stability verification.

Inputs: y (required), id, time, cutoff, +14 more. Returns: Fields: statspai://schema/result

regressA
Read-only

Fit an OLS regression; classical SEs unless robust= or cluster=. Input is a Wilkinson-style formula like 'y ~ x1 + x2'. Use this for baseline specifications or covariate-adjusted RCT analyses. Validation: certified parity evidence.

Inputs: formula (required), detail, robust, cluster, +7 more. Returns: Fields: statspai://schema/result

route_estimatorA
Read-only

Route a research question to estimator calls without data. Give the family (did / iv / rd / matching / ml_causal / qte / dynamic_panel) and answers to its decision questions; get the matching registered functions with example calls, why each is right, the assumptions it adds and the guide section to read, plus the unanswered questions and the one that narrows the choice most. Call with no answers to see the questions. The full guide is readable at statspai://guide/{family}.

Inputs: family (required), detail, answers, data_id, +6 more. Returns: Fields: statspai://schema/result

sdidA
Read-only

Synthetic difference-in-differences (Arkhangelsky et al. 2021): unit and time weights, one treated unit or block. Long panel; placebo / bootstrap / jackknife SEs. Validation: certified parity evidence.

Inputs: y (required), time (required), unit (required), seed, +12 more. Returns: Fields: statspai://schema/result

search_functionsA
Read-only

Search the StatsPAI function registry by task keywords or a short phrase (e.g. 'staggered adoption event study', 'weak instrument robust CI'). Returns ranked matches with one-line descriptions, category and validation tier. Use it when no listed tool fits, then describe_function for the parameters and call_function to run it.

Inputs: query (required), limit, detail, data_id, +7 more. Returns: Fields: statspai://schema/result

sensitivityB
Read-only

Unified sensitivity analysis for observational causal estimates -- supports Oster (2019) delta/R-max, Cinelli-Hazlett (2020) omitted-variable bias bounds, and E-values (VanderWeele-Ding 2017). Tells the agent how strong an unobserved confounder would have to be to overturn the result. Validation: certified parity evidence.

Inputs: y, treat, detail, result, +12 more. Returns: Fields: statspai://schema/result

sensitivity_from_resultA
Read-only

Sensitivity of a cached fit (result_id). 'evalue' (default): E-value from the estimate. 'oster' (delta / R-max) and 'cinelli_hazlett' (OVB bounds) refit OLS of y on treat + controls on the fit's data: from a regress formula (name the term with treat) or passed as y / treat / controls. 'auto': sensitivity dashboard.

Inputs: result_id (required), y, sd, treat, +11 more. Returns: Fields: statspai://schema/result

spec_curveA
Read-only

Specification-curve analysis (Simonsohn et al. 2020): enumerates every combination of model choices the user declares defensible, runs them all, and returns the sign/magnitude distribution. Use when an agent needs to report robustness across a researcher-degree-of-freedom multiverse. Validation: validated evidence tier (known-truth, reference, external-parity, or Monte Carlo artifact).

Inputs: x (required), y (required), alpha, detail, +10 more. Returns: Fields: statspai://schema/result

sun_abrahamA
Read-only

Sun-Abraham (2021) interaction-weighted event study for staggered adoption (Stata eventstudyinteract, fixest sunab). Cohort column g = first-treatment period, 0 / NaN never treated. Validation: certified parity evidence.

Inputs: g (required), i (required), t (required), y (required), +13 more. Returns: Fields: statspai://schema/result

synthA
Read-only

Synthetic control (Abadie et al.) and its variants through one entry: classic SCM with placebo inference, synthetic DiD, augmented / penalized SCM, generalized SC (gsynth), staggered adoption and more via method=. Panel in long format, one treated unit (staggered: a treatment column instead). Validation: certified evidence with scoped limitations. Known limitations: Classical SCM certification is specification-specific: ADH/Synth parity requires passing the same special_predictors recipe; the default outcome-only V=I path is a documented Kaul-style convention; Default native classical SCM can differ from Synth on Basque-style panels by a documented local-optimum convention (the outer V optimisation has multiple near-equivalent minima); use backend='synth' or canonical special_predictors when exact R parity is required.

Inputs: time (required), unit (required), outcome (required), detail, +13 more. Returns: Fields: statspai://schema/result

transform_dataA
Read-only

Derive a new dataset from a data_id (or data_path / inline data) by applying operations in order, and return a new data_id with the lineage recorded. Operations (each an object with op): query {expr} (pandas DataFrame.query), select {columns}, drop {columns}, rename {mapping}, dropna {columns?}, fillna {value | mapping}, assign {column, expr} (DataFrame.eval), sort {by, ascending?}, sample {n, seed?}, winsor {columns?, cuts?=[1,99], by?, trim?, method?} (replaces in place), wide_to_long {stubnames, i, j, sep?, suffix?}, long_to_wide {index, columns, values}, mice {columns?, m?} (single completed dataset), function {name, arguments?} (any sp. taking data= and returning a DataFrame). query / assign expressions are limited to columns (backticks for odd names), constants, operators, in / not in, math functions (log, exp, sqrt, abs, ...) and col.isnull()/isin()/between()/str.contains(); other attribute access, subscripts and @locals fail with error_kind='unsafe_expression'.

Inputs: operations (required), name, detail, data_id, +8 more. Returns: Fields: statspai://schema/result

vcov_from_resultA
Read-only

Standard errors of a cached fit (result_id) under another vce: re-runs the fit's own call with only vce= changed, so the SEs equal a direct refit. One vce returns the new result; a list returns a side-by-side table with the ratio to the fitted SE.

Inputs: vce (required), result_id (required), terms, detail, +6 more. Returns: Fields: statspai://schema/result

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    B3.4/5.0

    Scored across 45 tools

    Disambiguation3/5

    Descriptions work hard to cross-reference each other (e.g., brief says 'Same output as brief_result'; honest_did points to honest_did_from_result), but several pairs genuinely overlap: brief/brief_result, audit/audit_result, sensitivity/sensitivity_from_result, iv vs ivreg, and honest_did vs honest_did_from_result. The advisory cluster (detect_design, check_identification, preflight, recommend, route_estimator, causal) is also hard to separate without reading ordering hints, though the descriptions do provide them.

    Naming Consistency3/5

    Mostly snake_case, but two conventions coexist: verb_noun for plumbing (load_data, transform_data, search_functions, plot_from_result) and bare estimator/package names for methods (did, iv, dml, synth, match, sdid, feols, ivreg, rdrobust, ebalance). The result-accessor suffix is also inconsistent, mixing '_result' (brief_result, audit_result) with '_from_result' (honest_did_from_result, sensitivity_from_result, vcov_from_result, plot_from_result).

    Tool Count2/5

    45 individually listed tools is heavy for any single server, and several slots are redundant duplicates (brief vs brief_result, audit vs audit_result, sensitivity vs sensitivity_from_result) rather than earned surface. The search_functions/describe_function/call_function escape hatch already covers the ~1,200-function registry, so the long tail of listed estimator tools is partly unnecessary.

    Completeness4/5

    Coverage of the causal-inference lifecycle is strong: data load/transform/profile, design detection and preflight, estimators across DID/IV/RD/matching/DML/synth, robustness and sensitivity, pipelines, plotting, interpretation, and citation lookup. The main gap is state management — there is no way to list existing data_ids or result_ids, or to delete/export them, which forces agents to carry handles out-of-band.

    Maintenance

    ActivityActive
    ResponsivenessLimited data

    Related MCP Connectors

    Related MCP Servers

    • F
      license
      Not graded
      quality
      D
      maintenance
      Enables comprehensive statistical analysis including descriptive statistics, hypothesis testing, regression, and more via a FastMCP-based API.
      3
      -
    • F
      license
      Not graded
      quality
      D
      maintenance
      An MCP server that exposes causal inference methods (difference-in-differences, synthetic control, propensity matching, and assumption checks) as callable tools, enabling AI agents to run deterministic statistical analyses instead of computing them inline.
      -