Skip to main content
Glama
vikranthviki

Causal Decision Agent

by vikranthviki

bjs

Read-only

Estimate causal effects in staggered treatment adoption using imputation-based difference-in-differences, with event studies and pre-trend diagnostics that handle heterogeneous effects.

Instructions

Borusyak, Jaravel & Spiess (2024) imputation DID estimator. Do NOT use when: no never-treated or not-yet-treated observations remain -- the imputation model cannot be fit; the design is a simple 2x2 -- use sp.did(method='2x2'). Assumptions: Conditional parallel trends between treated and comparison groups absent treatment; No anticipation of treatment before its onset; Treatment effects may be heterogeneous across cohorts and time (no homogeneity required). Pre-conditions: Panel or repeated cross-section with a unit (or group) identifier and a time identifier; At least one never-treated or not-yet-treated comparison group; Pre-treatment periods to assess parallel trends. Failure modes: Pre-treatment event-study coefficients are jointly non-zero (pre-trend violation) -> Use honest DiD bounds to quantify robustness to trend violations, or condition on covariates; Two-way fixed-effects estimate is contaminated by 'forbidden' comparisons / negative weights -> Use a heterogeneity-robust estimator (Callaway-Sant'Anna, Borusyak et al., Gardner two-stage). Alternatives: sp.callaway_santanna, sp.did, sp.honest_did. Typical minimum N: 100.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
yYesOutcome variable name.
feNoWhich fixed effects the Y(0) model carries, replacing the default two-way ``unit + time``. Mirrors Stata ``did_imputation``'s ``fe()``. Each entry is one fixed effect, written either as a bare column or Stata-style with ``#`` for the interacted cell:: fe=['time'] # period FE only fe=['unit', 'state#year'] # unit FE + state-by-year FE fe=[] # no fixed effects at all Levels are factorized over the whole panel, so a cell seen only among treated rows still gets a column and is imputed rather than silently absorbing the reference level.
vceNoStandard-error mode for the overall ATT. ``'analytic'`` is the exact Borusyak--Jaravel--Spiess variance: the estimator is linear in the outcome, so its weights are computed rather than approximated, and the result reproduces Stata ``did_imputation`` and R ``didimputation`` to ~5e-8. Before v1.23.0 this path used an approximation that was materially anti-conservative (18-36% too small on the harness fixtures); see MIGRATION.md. Measured, not asserted. On a homogeneous-effect design with a never-treated half, 400-800 replications per cell: =========== ========== ==================== clusters coverage mean SE / sd(est) =========== ========== ==================== 30 0.925 0.985 60 0.938 0.936 120 0.948 0.978 240 0.935 1.007 480 **0.950** 0.983 =========== ========== ==================== Two things to read off it. The ratio sits at 1 throughout, so the variance formula carries no systematic bias -- the approximation it replaced ran 18-36% low. And coverage reaches nominal by 480 clusters, so the shortfall at 60 is a small-cluster effect rather than a missing term: there the standard error is simply *noisy* (dispersion 9.2% against 3.2% at 480), which fattens the studentised distribution (sd(t) = 1.04 against 1.007). A ``t(G-1)`` critical value barely helps (0.938 to 0.941 at G = 60) because the problem is the variability of the standard error, not the degrees of freedom. ``vce='bootstrap'`` is the remedy in small designs, and ``se_method='auto'`` selects it below 30 clusters. ``'bootstrap'`` resamples whole clusters and re-runs the full imputation estimator. Point estimates are identical either way; per-horizon event-study SEs are unaffected.analytic
timeYesTime period column.
alphaNoSignificance level for confidence intervals.
groupYesUnit identifier column.
hetbyNoStata ``did_imputation, hetby(varname)``: report heterogeneous overall ATTs by the levels of a **time-invariant** unit-level variable. Results land in ``model_info['hetby']`` (one row per
min_nNoStata ``did_imputation, minn(#)``: drop event-study horizons with fewer than ``min_n`` treated observations (they are noisy and dominated by a single cohort). Dropped horizons are listed in a warning and excluded from the pre-trend test.
detailNoPayload depth: 'minimal' (~150 tokens) for sub-step calls where only the point estimate is needed; 'standard' (~1K tokens) for diagnostics + coefficient table; 'agent' (~2K tokens, default) adds violations / next_steps / suggested_functions so the LLM can plan its next call without another round-trip.agent
n_bootNoNumber of cluster-bootstrap replications when ``vce='bootstrap'``.
clusterNoVariable for cluster-robust standard errors. Defaults to ``group`` (unit-level clustering).
horizonNoRelative time periods for event study estimates, e.g. ``list(range(-5, 6))``. If ``None``, reports only the overall ATT (no event study disaggregation).
projectNoproject parameter (Optional[List[str]]).
balancedNoStata ``did_imputation, hbalance``: keep only eventually-treated units observed at *every* requested horizon, so the event-study composition is stable across ``k`` (no cohort churn). Never-treated units are always kept. Requires ``horizon`` (or ``pretrends``). Warns with the number of units dropped.
controlsNoContinuous time-varying controls, entering the Y(0) model additively. Stata ``did_imputation``'s ``controls()``.
as_handleNoIf true, cache the fitted result on the server and return result_id + result_uri alongside the JSON payload so a subsequent tools/call can chain without re-running.
boot_seedNoSeed for the cluster bootstrap (deterministic results).
data_pathYesAbsolute path or URL to a data file. Supported: .csv / .tsv / .txt (delimited), .parquet / .pq, .feather / .arrow, .xlsx / .xls, .dta (Stata), .json / .jsonl. Schemes: file://, s3://, gs://, https://.
pretrendsNoStata ``did_imputation, pretrends(k)``: estimate the ``k`` pre-treatment placebo coefficients (horizons ``-k .. -1``, added to ``horizon`` if not already requested) and report their joint Wald test in ``model_info['pretrend_test']``. Under the default ``pretrend_method='bjs'`` the test uses the full cluster-robust covariance of the auxiliary lead regression; under the other conventions it assumes the lead estimates are uncorrelated (valid but conservative), and :func:`statspai.bjs_pretrend_joint` gives the covariance-aware cluster-bootstrap version.
result_idNoOptional handle to a previously-fitted result (returned by an earlier call when as_handle=true). Tools that operate on a fitted object accept this in place of re-supplying data_path + columns.
se_methodNoShared DiD spelling for ``vce=`` -- ``'analytic'``, ``'bootstrap'`` (also ``'cluster'``, ``'pairs'``) or ``'auto'``. Passing both raises. ``'auto'`` picks the cluster bootstrap when the design has at most 30 clusters (Cameron, Gelbach & Miller 2008) and the analytic BJS variance otherwise: with few clusters the cluster-score sum behind any sandwich is itself noisy, which is a small-cluster problem rather than a defect in the formula.
first_treatYesColumn indicating the period of first treatment. Use ``np.inf``, ``np.nan``, or ``0`` for never-treated units.
data_columnsNoOptional column projection. Parquet/Feather/Stata loaders honour this for fast partial reads.
save_weightsNoStata ``did_imputation, saveweights()``: store the exact estimation weights ``w`` such that ``ATT = w'y`` in ``model_info['estimation_weights']`` (aligned with the rows of ``data``). Treated rows get ``1/N1``; untreated rows get the (negative) imputation weights implied by the FE projection -- useful for diagnosing which comparisons drive the estimate.
data_sample_nNoOptional uniform random subsample size (seed=0, deterministic) — useful on huge panels.
save_residualsNoStata ``did_imputation, saveresid()``: store the untreated-fit residuals ``y - y0`` in ``model_info['residuals']`` (aligned with the rows of ``data``; ``NaN`` on treated rows, whose ``y - y0`` is the treatment effect, not a residual).
pretrend_methodNoHow the **pre-treatment** event-study coefficients are built. The post-treatment coefficients are imputation residuals either way; only the leads differ, and the difference is visible in the plot rather than in the ATT. - ``'bjs'`` -- the convention of Stata ``did_imputation, pretrends(k)``: an auxiliary dynamic TWFE regression on the untreated observations, with all relative times earlier than the requested leads pooled into the omitted category. Reproduces Stata's coefficients and standard errors. Not available with ``fe=``, ``unit_covariates=`` or ``time_covariates=``. - ``'in-sample'`` -- average the imputation residuals at pre-treatment relative times, as ``fect`` and ``did2s`` do. These are in-sample prediction errors: in a non-staggered design they equal the symmetric benchmark times the untreated unit share ``N0/N``, so they understate pre-trends, severely when most units are treated. This was StatsPAI's behaviour before v1.23.0. - ``'symmetric'`` -- Roth's (2026) repair, which uses the average of the pre-treatment periods as the reference for both halves of the path, so the plot matches a dynamic TWFE event study up to a common vertical shift and the usual visual heuristics apply. Non-staggered balanced designs without covariates only; raises otherwise rather than applying an unverified factor. The chosen convention and its caveat are recorded in ``model_info['pretrend_method']`` and ``model_info['event_study_convention']``.bjs
time_covariatesNoControls interacted with the **period** fixed effects, i.e. one coefficient per period. Stata's ``timecontrols()``. Typically a time-invariant unit characteristic whose effect is allowed to move over calendar time.
unit_covariatesNoControls interacted with the **unit** fixed effects, i.e. one slope per unit. Stata's ``unitcontrols()``. The canonical use is ``unit_covariates=[time_col]``, which makes Y(0) carry unit-specific linear trends. Identification bites hard here: a unit-specific slope needs at least two untreated periods for that unit, so early-treated cohorts can lose their imputation entirely.

Output Schema

TableJSON Schema
NameRequiredDescriptionDefault

No arguments

Schema Changelog

Changes observed during successful MCP inspections.

  1. First observedv0.1.0

TDQS

A4.7/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations declare readOnlyHint=true and openWorldHint=false, and the description does not contradict them. Beyond that, it discloses key behavioral properties: failure modes (pre-trend violations, TWFE contamination), the imputation model's requirements, and assumptions like no anticipation and heterogeneous effects. This gives the agent a clear picture of how the estimator behaves and where it breaks.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

While long, the description is tightly organized into labeled sections (Do NOT use, Assumptions, Pre-conditions, Failure modes, Alternatives, Typical minimum N) with no filler. The most decision-relevant information is front-loaded, and every sentence contributes to correct tool selection and invocation.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's complexity (29 parameters, 5 required, rich output schema), the description supplies the essential selection criteria, assumptions, failure modes, and alternatives. It does not need to describe return values because an output schema exists and is rich. The description is complete enough for an agent to decide whether and when to call this tool.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the input schema already documents all parameters in detail. The tool-level description adds general pre-conditions and failure modes but does not elaborate on individual parameters beyond what the schema already provides. This matches the baseline of 3 where the schema carries the parameter-semantics burden.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description names the estimator explicitly ('Borusyak, Jaravel & Spiess (2024) imputation DID estimator') and clarifies it is a DiD estimator, so an agent knows exactly what resource this tool targets. It also distinguishes itself from alternatives and states specific exclusions (e.g., simple 2x2 designs should use sp.did), making it easy to differentiate from siblings.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description gives explicit 'Do NOT use when' conditions, enumerates assumptions and pre-conditions, names alternatives such as sp.callaway_santanna and sp.honest_did, and even specifies a typical minimum N. This is comprehensive guidance for when to select this tool versus other DiD estimators.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Deploy Server

Other Tools