Skip to content

statspai.smart

smart

Smart Workflow Engine.

Registered workflow helpers for planning, diagnostics, sensitivity, and replication support:

  • recommend() — DAG + data → estimator selection
  • compare_estimators() — run multiple methods, compare, diagnose
  • assumption_audit() — comprehensive assumption testing by method
  • sensitivity_dashboard() — multi-dimensional sensitivity analysis
  • pub_ready() — journal-specific publication readiness checklist
  • replicate() — famous paper replication with built-in data

AssumptionResult

Bases: ResultProtocolMixin

Results from comprehensive assumption audit.

Returned by :func:statspai.assumption_audit. Bundles the per-check diagnostics (checks), an overall letter overall_grade (A–F), pass/fail/inconclusive counts, and any critical_failures. Use :meth:summary for a printable report, :meth:failed to list failed checks with remedies, and :meth:passed_all for a quick gate.

Examples:

>>> import statspai as sp
>>> import numpy as np, pandas as pd
>>> rng = np.random.default_rng(0)
>>> n = 200
>>> educ = rng.integers(8, 20, n)
>>> exper = rng.integers(0, 40, n)
>>> wage = 1.0 + 0.1 * educ + 0.05 * exper + rng.normal(0, 1, n)
>>> df = pd.DataFrame({"wage": wage, "educ": educ, "exper": exper})
>>> result = sp.regress("wage ~ educ + exper", data=df)
>>> audit = sp.assumption_audit(result, data=df, verbose=False)
>>> type(audit).__name__
'AssumptionResult'
>>> bool(audit.overall_grade in set("ABCDF"))
True
>>> isinstance(audit.failed(), list)
True

failed

failed() -> List[AssumptionCheck]

Return only failed checks.

passed_all

passed_all() -> bool

True if all checks passed.

ComparisonResult

Bases: ResultProtocolMixin

Results from multi-estimator comparison.

Returned by :func:compare_estimators. Bundles the per-method fitted results (.results), a tidy estimates_table DataFrame, and a dict of cross-method agreement diagnostics. Use .summary() for a text report and .plot() for a forest plot.

Examples:

>>> import statspai as sp
>>> import numpy as np, pandas as pd
>>> rng = np.random.default_rng(0)
>>> n = 300
>>> age = rng.normal(40, 10, n)
>>> educ = rng.normal(12, 3, n)
>>> ps = 1 / (1 + np.exp(-(0.05 * (age - 40) + 0.1 * (educ - 12))))
>>> training = rng.binomial(1, ps)
>>> wage = (5 + 2.0 * training + 0.1 * age + 0.3 * educ
...         + rng.normal(0, 1, n))
>>> df = pd.DataFrame({"wage": wage, "training": training,
...                    "age": age, "education": educ})
>>> comp = sp.compare_estimators(
...     data=df, y="wage", treatment="training",
...     methods=["ols"], covariates=["age", "education"],
... )
>>> type(comp).__name__
'ComparisonResult'
>>> comp.n_obs
300
>>> list(comp.estimates_table.columns)[:2]
['method', 'estimate']

plot

plot(ax: Optional[Any] = None, **kwargs: Any) -> Any

Forest plot comparing estimates across methods.

DiagnosticFinding dataclass

A single design-level finding.

These are the elements of :attr:IdentificationReport.findings; each carries a severity ('blocker' / 'warning' / 'info'), a category, a human-readable message and an optional suggestion.

Examples:

>>> import statspai as sp
>>> f = sp.DiagnosticFinding(
...     severity="warning", category="power",
...     message="Minimum detectable effect is large.",
...     suggestion="Collect more units.")
>>> f.severity, f.category
('warning', 'power')
>>> f.icon
'[!]'

IdentificationError

Bases: IdentificationFailure

Raised by check_identification(strict=True) when a blocker is found.

Carries the full :class:IdentificationReport on self.report so downstream code can still inspect findings without re-running. As a subclass of :class:statspai.IdentificationFailure it is catchable through the central taxonomy — except sp.IdentificationFailure and except sp.StatsPAIError both fire — and it carries the standard recovery_hint / diagnostics payload an agent can branch on.

Examples:

>>> import statspai as sp
>>> rep = sp.IdentificationReport(
...     findings=[sp.DiagnosticFinding(
...         severity="blocker", category="variation",
...         message="No variation in treatment.")],
...     design="cross_section", n_obs=30)
>>> err = sp.IdentificationError(rep)
>>> isinstance(err, sp.StatsPAIError)
True
>>> err.report.verdict
'BLOCKERS'

IdentificationReport dataclass

Report from check_identification.

Aggregates a list of :class:DiagnosticFinding into an overall :attr:verdict ('OK' / 'WARNINGS' / 'BLOCKERS') and offers :meth:summary for a printable digest and :meth:by_category to filter findings. You usually obtain one from sp.check_identification rather than constructing it directly.

Examples:

>>> import statspai as sp
>>> rep = sp.IdentificationReport(
...     findings=[
...         sp.DiagnosticFinding(
...             severity="warning", category="power",
...             message="Minimum detectable effect is large."),
...     ],
...     design="cross_section", n_obs=120)
>>> rep.verdict
'WARNINGS'
>>> len(rep.by_category("power"))
1
>>> bool("Identification Diagnostics" in rep.summary())
True

verdict property

verdict: str

Overall verdict: BLOCKERS | WARNINGS | OK.

IntakeResult dataclass

Bases: ResultProtocolMixin

Structured routing outcome before estimator selection.

Examples:

>>> import statspai as sp
>>> result = sp.IntakeResult(outcome="matched", recommended_design="rd")
>>> result.to_dict()["recommended_design"]
'rd'

MethodSpec dataclass

A verified methods-appendix entry for one estimator family.

Attributes:

Name Type Description
key str

Canonical lookup key (matches the citation key where possible).

name str

Human-readable estimator name for the section heading.

estimand_latex str

LaTeX (no delimiters) for the target parameter.

estimator_latex str

LaTeX (no delimiters) for the sample estimator.

prose str

One- to three-sentence plain-language description of the estimator.

assumptions list of str

Identifying assumptions, one short clause each.

aliases list of str

Extra keys / substrings that should resolve to this spec.

PubReadyResult

Bases: ResultProtocolMixin

Publication readiness checklist results.

Returned by :func:pub_ready. Holds the venue, the resolved checklist items (each flagged done / not done), a 0-100 readiness score, and the lists of missing and present items. Call :meth:summary for a terminal-friendly report.

Examples:

>>> import statspai as sp
>>> df = sp.cps_wage()
>>> res = sp.regress("log_wage ~ education + experience", data=df)
>>> check = sp.pub_ready(results=[res], venue="aej_applied",
...                      has_robustness=True)
>>> type(check).__name__
'PubReadyResult'
>>> bool(0 <= check.score <= 100)
True
>>> bool(isinstance(check.missing, list))
True

RecommendationResult

Bases: ResultProtocolMixin

Result from the estimator recommendation engine.

Returned by :func:recommend. Holds a ranked list of estimator recommendations (each a dict with method / function / reason / assumptions), the detected design, the data_profile (outcome / treatment types, panel shape, missingness), and any warnings. Call :meth:summary for a ranked, human-readable report.

Examples:

>>> import statspai as sp
>>> df = sp.cps_wage()
>>> rec = sp.recommend(df, y="log_wage", treatment="union",
...                    covariates=["education", "experience"])
>>> type(rec).__name__
'RecommendationResult'
>>> bool(len(rec.recommendations) > 0)
True
>>> bool("method" in rec.recommendations[0])
True

to_latex

to_latex(caption: Optional[str] = None, label: str = 'tab:recommendation') -> str

Export recommendations as a booktabs LaTeX table.

If verify=True was used when calling recommend(), the table includes the stability-check columns (composite score, bootstrap stability, placebo pass-rate, subsample agreement).

IMPORTANT CAVEAT FOR AUTHORS: The stability score measures whether a method gives consistent estimates under resampling on the observed data — it does NOT establish identification validity or protect against unobserved confounding. A biased OLS on observational data will typically score high because the bias is stable across resamples. Do not cite this score as evidence that a method is "correct" for a given design; use it only to compare the stability of methods that already satisfy the design's identification assumptions.

Parameters:

Name Type Description Default
caption str

Table caption. Defaults to the detected design.

None
label str

LaTeX label for cross-referencing.

'tab:recommendation'

Returns:

Type Description
str

LaTeX source (booktabs + threeparttable).

run

run(which: int = 0, **kwargs: Any) -> Any

Execute the recommended estimator.

Parameters:

Name Type Description Default
which int

Which recommendation to run (0 = top recommendation).

0
**kwargs Any

Override any parameters.

{}

run_all

run_all(**kwargs: Any) -> Dict[str, Any]

Run all recommended estimators and return a comparison.

SensitivityDashboard

Multi-dimensional sensitivity analysis results.

Returned by :func:sp.sensitivity_dashboard. Holds the baseline estimate, a list of per-dimension summaries and an overall A/B/C/D/F stability grade.

Examples:

>>> import numpy as np
>>> import pandas as pd
>>> import statspai as sp
>>> rng = np.random.default_rng(0)
>>> n = 200
>>> df = pd.DataFrame({
...     "x1": rng.normal(size=n),
...     "x2": rng.normal(size=n),
... })
>>> df["y"] = 1.0 + 0.5 * df["x1"] - 0.3 * df["x2"] + rng.normal(size=n)
>>> result = sp.regress("y ~ x1 + x2", data=df)
>>> dash = sp.sensitivity_dashboard(result, data=df, verbose=False)
>>> type(dash).__name__
'SensitivityDashboard'
>>> bool(dash.overall_stability in {"A", "B", "C", "D", "F"})
True
>>> isinstance(dash.dimensions, list)
True

assumption_audit

assumption_audit(result: Any, data: Optional[DataFrame] = None, alpha: float = 0.05, verbose: bool = True) -> AssumptionResult

Comprehensive assumption audit for any estimated model.

Run the method's registered assumption checks and provide actionable remedies for failed or inconclusive diagnostics.

Parameters:

Name Type Description Default
result EconometricResults or CausalResult

Estimated model result.

required
data DataFrame

Original data (needed for some tests). Auto-extracted if available.

None
alpha float

Significance level for tests.

0.05
verbose bool

Print summary automatically.

True

Returns:

Type Description
AssumptionResult

With .summary(), .failed(), .passed_all() methods.

Examples:

>>> import statspai as sp
>>> import numpy as np, pandas as pd
>>> rng = np.random.default_rng(0)
>>> n = 200
>>> df = pd.DataFrame({
...     "educ": rng.integers(8, 18, n).astype(float),
...     "exper": rng.integers(0, 30, n).astype(float),
... })
>>> df["wage"] = (5 + 0.4 * df["educ"] + 0.1 * df["exper"]
...               + rng.normal(0, 1, n))
>>> result = sp.regress("wage ~ educ + exper", data=df)
>>> audit = sp.assumption_audit(result, data=df, verbose=False)
>>> _ = audit.summary()
>>> if not audit.passed_all():
...     for fail in audit.failed():
...         _ = fail.remedy

bib_for

bib_for(result: Any) -> Dict[str, Any]

Top-level structured citation for a fitted result.

Convenience entry that pairs with result.cite(format="json") so agents that don't have direct access to the result method can pull the structured payload via sp.bib_for(...) instead.

Parameters:

Name Type Description Default
result CausalResult or EconometricResults

Any fitted result object exposing a .cite() method.

required

Returns:

Type Description
dict

Same shape as result.cite(format="json"): {type, key, authors, year, title, journal, volume, number, pages, publisher, fields}.

Examples:

>>> import statspai as sp
>>> import numpy as np
>>> import pandas as pd
>>> rng = np.random.default_rng(5)
>>> rows = []
>>> for i in range(200):
...     tr = 1 if i < 100 else 0
...     for t in (0, 1):
...         y = (1.0 + 0.3 * t + 0.5 * tr + 2.0 * tr * t
...              + rng.normal(scale=0.5))
...         rows.append({'i': i, 't': t, 'treated': tr, 'post': t, 'y': y})
>>> df = pd.DataFrame(rows)
>>> r = sp.did(df, y='y', treat='treated', time='t', post='post')
>>> sp.bib_for(r)['key']
'angrist2009mostly'

bibtex

bibtex(keys: Union[str, Iterable[str]]) -> str

Resolve verified BibTeX entries from paper.bib by citation key.

The Python twin of the bibtex MCP tool, and the resolver that :func:bib_for and result.cite(format="json") advertise in their resolve_with hint: feed the citation_keys they return straight into sp.bibtex to obtain the full, verified @article{...} entries from the project's single bibliographic source of truth (paper.bib). Entries are returned verbatim — never reformatted or invented (CLAUDE.md §10 zero-hallucination rule).

Parameters:

Name Type Description Default
keys str or iterable of str

One or more BibTeX keys, e.g. "chernozhukov2016hdm" or ["callaway2021difference", "rambachan2023more"].

required

Returns:

Type Description
str

The matching entries in the order requested, separated by a blank line, ready to paste into a .bib file.

Raises:

Type Description
ValueError

If keys is empty.

KeyError

If any requested key is absent from paper.bib; the message lists the missing keys together with any close matches, so a typo is corrected rather than papered over with a fabricated entry.

See Also

bib_for : structured citation payload (and citation_keys) for a fitted result; pipe its keys into this function.

Examples:

>>> import statspai as sp
>>> entry = sp.bibtex("chernozhukov2016hdm")
>>> all(
...     x in entry
...     for x in ("Chernozhukov", "Hansen", "Spindler", "10.32614/RJ-2016-040")
... )
True

render_citation

render_citation(bibtex: str, fmt: str = 'bibtex') -> Any

Render a stored BibTeX string in the requested format.

Parameters:

Name Type Description Default
bibtex str

Raw BibTeX entry as stored on the result class. May contain multiple @type{...} entries concatenated (some methods cite more than one paper); the renderer walks every entry.

required
fmt ('bibtex', 'apa', 'json')

Output format.

"bibtex"

Returns:

Type Description
str | dict | list
  • "bibtex"str (the raw string, multi-entry preserved as-is).
  • "apa"str (single entry) or str with entries joined by a blank line (multi-entry).
  • "json"dict for single-entry input (the original shape, preserved for backward compat) OR list[dict] when the source contains multiple BibTeX entries.

compare_estimators

compare_estimators(data: DataFrame, y: str, treatment: str, methods: Optional[List[str]] = None, covariates: Optional[List[str]] = None, id: Optional[str] = None, time: Optional[str] = None, instrument: Optional[str] = None, alpha: float = 0.05, method_hints: Optional[Dict[str, Dict[str, Any]]] = None) -> ComparisonResult

Run multiple estimators on the same data and compare.

Run selected estimators and return an agreement-diagnostics table for manual robustness review.

Parameters:

Name Type Description Default
data DataFrame
required
y str

Outcome variable.

required
treatment str

Treatment variable (binary).

required
methods list of str

Estimators to compare. Default auto-selects based on data. Classical options: 'ols', 'matching', 'ipw', 'aipw', 'dml', 'g_computation', 'causal_forest', 'did', 'panel_fe'.

Hint-driven Sprint-B options (require method_hints): 'proximal', 'msm', 'principal_strat', 'mediate', 'mediate_interventional', 'front_door'. Each needs method-specific kwargs the shared signature does not expose (proxy_z/proxy_w, time_varying, strata, mediator, etc.) — pass them through method_hints.

None
method_hints dict

Per-method keyword overrides, merged with the shared kwargs when dispatching each estimator. Structure::

{'proximal': {'proxy_z': ['z'], 'proxy_w': ['w']},
 'msm':      {'time_varying': ['L_lag']},
 'principal_strat': {'strata': 's'}}

Collision rule (docs/ROADMAP.md §6): per-method hints take precedence over the shared kwargs for the method they name. If the top-level covariates=['age'] disagrees with method_hints={'proximal': {'covariates': ['age', 'educ']}}, proximal uses the hint and every other method uses the shared arg. A UserWarning fires on conflict so the override is visible in the log.

None
covariates list of str
None
id str

Panel unit ID.

None
time str

Time variable.

None
instrument str
None
alpha float
0.05

Returns:

Type Description
ComparisonResult

With .summary(), .plot(), .results (dict of individual results).

Examples:

>>> import statspai as sp
>>> import numpy as np, pandas as pd
>>> rng = np.random.default_rng(0)
>>> n = 300
>>> age = rng.normal(40, 10, n)
>>> educ = rng.normal(12, 3, n)
>>> ps = 1 / (1 + np.exp(-(0.05 * (age - 40) + 0.1 * (educ - 12))))
>>> training = rng.binomial(1, ps)
>>> wage = (5 + 2.0 * training + 0.1 * age + 0.3 * educ
...         + rng.normal(0, 1, n))
>>> df = pd.DataFrame({"wage": wage, "training": training,
...                    "age": age, "education": educ})
>>> comp = sp.compare_estimators(
...     data=df, y="wage", treatment="training",
...     methods=["ols"], covariates=["age", "education"],
... )
>>> type(comp).__name__
'ComparisonResult'
>>> comp.n_obs
300

check_identification

check_identification(data: DataFrame, y: str, treatment: Optional[str] = None, covariates: Optional[List[str]] = None, id: Optional[str] = None, time: Optional[str] = None, running_var: Optional[str] = None, instrument: Optional[str] = None, cluster: Optional[str] = None, cutoff: Optional[float] = None, design: Optional[str] = None, cohort: Optional[str] = None, dag: Any = None, strict: bool = False) -> IdentificationReport

Run design-level identification diagnostics before fitting an estimator.

This reads your dataframe + design and outputs a prioritised list of pitfalls — bad controls, overlap violations, underpowered designs, small cohorts, clustering ambiguity.

Parameters:

Name Type Description Default
data DataFrame
required
y str

Outcome column.

required
treatment str

Binary or continuous treatment column.

None
covariates list of str

Candidate control variables.

None
id str

Panel identifiers.

None
time str

Panel identifiers.

None
running_var str

RD running variable.

None
instrument str

IV instrument.

None
cluster str

Clustering column for inference.

None
cutoff float

RD cutoff value.

None
design str

Override auto-detected design: one of 'rct', 'did', 'rd', 'iv', 'observational', 'panel'.

None
cohort str

First-treatment-period column (for staggered DID).

None
dag DAG

Causal DAG. If supplied, runs Cinelli-Forney-Pearl (2022) bad-control detection (mediator, descendant, collider, M-bias) and verifies the covariate set satisfies a valid adjustment criterion. Upgrades correlation heuristic to a principled check.

None
strict bool

If True, raise :class:IdentificationError when the report's verdict is 'BLOCKERS'. Use in CI / automated pipelines where you want a hard failure when the design is broken. The exception carries .report for post-mortem inspection.

False

Returns:

Type Description
IdentificationReport

With .summary(), .verdict, .findings, .by_category().

Notes

When a supplied dag object errors during a sub-check (bad-control analysis or adjustment-set verification), that sub-check degrades to an info-level finding plus a WorkflowDegradedWarning rather than failing the whole report; the verdict is unaffected.

Examples:

>>> import statspai as sp
>>> import numpy as np, pandas as pd
>>> rng = np.random.default_rng(0)
>>> rows = []
>>> for w in range(40):
...     treated, age = w < 20, rng.integers(25, 55)
...     educ = rng.integers(10, 18)
...     for yr in (2000, 2001):
...         eff = 1.0 if (treated and yr == 2001) else 0.0
...         rows.append({"worker": w, "year": yr,
...                      "wage": 10 + 0.1 * age + 0.3 * educ + eff
...                              + rng.normal(0, 1),
...                      "training": int(treated),
...                      "age": age, "education": educ})
>>> df = pd.DataFrame(rows)
>>> report = sp.check_identification(
...     df, y="wage", treatment="training",
...     covariates=["age", "education"],
...     id="worker", time="year", design="did",
... )
>>> type(report).__name__
'IdentificationReport'
>>> bool(report.verdict in ("OK", "WARNINGS", "BLOCKERS"))
True

design_intake

design_intake(*, estimand: Optional[str] = None, assignment: Optional[str] = None, data_topology: Optional[str] = None, controls: Optional[str] = None, identification_support: Optional[str] = None, needs: Optional[str] = None) -> IntakeResult

Route design facts to a method-selection status.

This is a pre-recommend intake helper. It does not fit models and it does not pretend missing identification facts are harmless.

Examples:

>>> import statspai as sp
>>> intake = sp.design_intake(
...     assignment="threshold cutoff",
...     data_topology="cross-section",
...     identification_support="continuity",
... )
>>> intake.outcome
'matched'
>>> intake.recommended_design
'rd'

pub_ready

pub_ready(results: Optional[list] = None, venue: str = 'top5_econ', design: Optional[str] = None, has_balance: bool = False, has_pretrends: bool = False, has_robustness: bool = False, has_heterogeneity: bool = False, has_sensitivity: bool = False, has_placebo: bool = False, has_mht: bool = False) -> PubReadyResult

Publication readiness checklist.

Generate a venue-specific checklist for empirical paper submission.

Parameters:

Name Type Description Default
results list

List of estimated result objects.

None
venue str

Target venue: 'top5_econ', 'aej_applied', 'rct'.

'top5_econ'
design str

Research design: 'rct', 'did', 'rd', 'iv', 'observational'.

None
has_balance bool

Already have balance table.

False
has_pretrends bool

Already have pre-trend tests.

False
has_robustness bool

Already have robustness checks.

False
has_heterogeneity bool

Already have subgroup analysis.

False
has_sensitivity bool

Already have sensitivity analysis.

False
has_placebo bool

Already have placebo tests.

False
has_mht bool

Already have MHT correction.

False

Returns:

Type Description
PubReadyResult

Examples:

>>> import statspai as sp
>>> df = sp.cps_wage()
>>> res = sp.regress("log_wage ~ education + experience", data=df)
>>> check = sp.pub_ready(results=[res], venue="aej_applied",
...                      has_robustness=True)
>>> bool(0 <= check.score <= 100)
True
>>> bool(isinstance(check.missing, list))
True

list_replications

list_replications() -> DataFrame

List all available replication datasets and guides.

Returns:

Type Description
DataFrame

Columns: key, title, design, journal, n_obs, has_real_data, has_classic_track, has_modern_track.

Examples:

>>> import statspai as sp
>>> sp.list_replications()

sensitivity_dashboard

sensitivity_dashboard(result: Any, data: Optional[DataFrame] = None, dimensions: Optional[List[str]] = None, alpha: float = 0.05, verbose: bool = True) -> SensitivityDashboard

Comprehensive multi-dimensional sensitivity analysis.

Test sensitivity across selected dimensions and produce an overall stability grade.

Parameters:

Name Type Description Default
result EconometricResults or CausalResult

Baseline estimated result.

required
data DataFrame

Original data (auto-extracted if possible).

None
dimensions list of str

Which dimensions to test. Default: all applicable. Options: 'sample', 'controls', 'functional_form', 'outliers', 'unobservables'.

None
alpha float
0.05
verbose bool
True

Returns:

Type Description
SensitivityDashboard

Examples:

>>> import numpy as np
>>> import pandas as pd
>>> import statspai as sp
>>> rng = np.random.default_rng(0)
>>> n = 200
>>> df = pd.DataFrame({
...     "x1": rng.normal(size=n),
...     "x2": rng.normal(size=n),
... })
>>> df["y"] = 1.0 + 0.5 * df["x1"] - 0.3 * df["x2"] + rng.normal(size=n)
>>> result = sp.regress("y ~ x1 + x2", data=df)
>>> dash = sp.sensitivity_dashboard(
...     result, data=df, dimensions=["sample", "outliers"], verbose=False,
... )
>>> type(dash).__name__
'SensitivityDashboard'