diff --git a/docs/api/had.rst b/docs/api/had.rst index f63722d7..8eff00b2 100644 --- a/docs/api/had.rst +++ b/docs/api/had.rst @@ -46,12 +46,38 @@ Unit Remains Untreated" (arXiv:2405.04465v6), which: - **Unweighted** - continuous paths use the CCT-2014 weighted-robust SE from the in-house ``lprobust`` port; the mass-point path uses a structural-residual 2SLS sandwich. No cross-horizon covariance. - - **``weights=`` shortcut** - continuous paths reuse the CCT-2014 SE; - the mass-point path uses an analytical weighted 2SLS sandwich - (``classical`` / ``hc1`` only - ``hc2`` / ``hc2_bm`` raise - ``NotImplementedError`` pending a 2SLS-specific leverage derivation). - - **``survey=``** - both paths compose Binder (1983) Taylor-series - linearization with ``df_survey`` threaded into ``safe_inference``. + - **``weights=np.ndarray`` shortcut (deprecated)** - continuous paths + reuse the CCT-2014 SE; the mass-point path uses an analytical + weighted 2SLS sandwich (``classical`` / ``hc1``; CR1 when + ``cluster=`` is supplied, except ``cluster=`` + + ``aggregate="event_study"`` + ``cband=True`` is rejected outright + regardless of ``vcov_type`` per the cluster-combination deviation + below; ``hc2`` / ``hc2_bm`` raise ``NotImplementedError`` pending a + 2SLS-specific leverage derivation). Yields + ``variance_formula="pweight"`` / ``"pweight_2sls"``. + - **``survey_design=SurveyDesign(weights="col", ...)``** (canonical; + accepts strata / PSU / FPC) - both paths compose Binder (1983) + Taylor-series linearization with ``df_survey`` threaded into + ``safe_inference``. Yields ``variance_formula="survey_binder_tsl"`` + / ``"survey_binder_tsl_2sls"``. + + The two weighted paths currently produce different SE families on this + estimator (CCT-2014 / 2SLS pweight-sandwich vs Binder-TSL); the + deprecated ``weights=`` and ``survey=`` aliases will be removed in the + next minor release, at which point the long-term unification onto a + single SE contract under ``survey_design=`` lands. (Tracked in + ``TODO.md``; the deprecation warning emitted by ``HeterogeneousAdoptionDiD.fit`` + spells the migration out per call site.) On array-in HAD pretest + helpers (``stute_test``, ``yatchew_hr_test``, ``stute_joint_pretest``) + the pweight-only shortcut is + ``survey_design=make_pweight_design(weights)``; data-in surfaces use + ``survey_design=SurveyDesign(weights="col_name", ...)`` against + ``data`` instead. ``qug_test`` is the exception: the QUG step has no + survey-aware migration target (Phase 4.5 C0 decision; see methodology + REGISTRY) and permanently raises ``NotImplementedError`` on any of + ``survey_design=`` / ``survey=`` / ``weights=``. The composite + workflow ``did_had_pretest_workflow`` handles this by skipping QUG + under survey/weighted dispatch and emitting a ``UserWarning``. A simultaneous confidence band (sup-t) is available only on the **weighted event-study path** via ``cband=True``. Joint cross-horizon @@ -59,16 +85,36 @@ Unit Remains Untreated" (arXiv:2405.04465v6), which: ``TODO.md``. **Mass-point ``vcov_type="classical"`` deviation.** The mass-point - ``survey=`` paths (static and event-study) and the ``weights=`` + - ``aggregate="event_study"`` + ``cband=True`` path reject - ``vcov_type="classical"`` with ``NotImplementedError``. The per-unit - 2SLS influence function returned by the mass-point fit is HC1-scaled - so that ``compute_survey_if_variance`` and the sup-t bootstrap target + ``survey_design=SurveyDesign(...)`` paths (static and event-study) and + the deprecated ``weights=`` + ``aggregate="event_study"`` + + ``cband=True`` path reject ``vcov_type="classical"`` with + ``NotImplementedError``. The per-unit 2SLS influence function returned + by the mass-point fit is HC1-scaled so that + ``compute_survey_if_variance`` and the sup-t bootstrap target ``V_HC1`` consistently; mixing it with a classical analytical SE would silently report a ``V_HC1``-targeted variance under a - ``classical`` label. Use ``vcov_type="hc1"`` (or leave it unset with - the default ``robust=True`` mapping); a classical-aligned IF - derivation is queued for a follow-up PR. + ``classical`` label. Use ``vcov_type="hc1"`` or set ``robust=True`` + explicitly (the constructor default ``robust=False`` maps to + ``vcov_type="classical"``, which triggers the guard); a + classical-aligned IF derivation is queued for a follow-up PR. + + **Mass-point cluster-combination deviation.** On + ``design="mass_point"``, two clustered weighted paths are rejected + outright regardless of ``vcov_type``: + + - ``survey_design=SurveyDesign(...)`` + ``cluster=`` (static and + event-study): the survey path composes Binder-TSL variance, which + would silently override the CR1 cluster-robust sandwich. + Workarounds: ``cluster=`` alone (unweighted CR1), or ``weights=`` + + ``cluster=`` (weighted-CR1 pweight sandwich), or + ``survey_design=`` alone (Binder-TSL). Combined cluster-robust + + survey inference is queued for a follow-up PR. + - Deprecated ``weights=`` shortcut + ``cluster=`` + + ``aggregate="event_study"`` + ``cband=True``: the sup-t bootstrap + normalizes HC1-scale perturbations by the CR1 analytical SE, + mixing variance families. Workarounds: pass ``cband=False`` (keeps + weighted-CR1 per-horizon), or drop ``cluster=`` (keeps + weighted-HC1 sup-t). HeterogeneousAdoptionDiD ------------------------ @@ -97,3 +143,63 @@ Multi-period event-study results container for the Appendix B.2 extension. :members: :undoc-members: :show-inheritance: + +HAD Pretests +------------ + +Diagnostic pretests for the HAD identification assumptions from de Chaisemartin +et al. (2026). The composite orchestrator +:func:`~diff_diff.did_had_pretest_workflow` is a diagnostic battery only - it +does NOT pick the HAD design path (``continuous_at_zero`` / +``continuous_near_d_lower`` / ``mass_point``); that is auto-detected inside +:meth:`HeterogeneousAdoptionDiD.fit` from the dose support. The workflow has +two explicit modes selected by the caller via the ``aggregate=`` kwarg: +``aggregate="overall"`` (default, two-period first-differenced sample) runs +single-period tests; ``aggregate="event_study"`` (multi-period panel with +three or more periods) runs joint multi-period tests. Both modes return a +unified :class:`~diff_diff.HADPretestReport`. + +.. autofunction:: diff_diff.did_had_pretest_workflow + +.. autoclass:: diff_diff.HADPretestReport + :members: + :undoc-members: + :show-inheritance: + +Single-period tests (``aggregate="overall"``) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. autofunction:: diff_diff.qug_test + +.. autofunction:: diff_diff.stute_test + +.. autofunction:: diff_diff.yatchew_hr_test + +.. autoclass:: diff_diff.QUGTestResults + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: diff_diff.StuteTestResults + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: diff_diff.YatchewTestResults + :members: + :undoc-members: + :show-inheritance: + +Joint multi-period tests (``aggregate="event_study"``) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. autofunction:: diff_diff.stute_joint_pretest + +.. autofunction:: diff_diff.joint_pretrends_test + +.. autofunction:: diff_diff.joint_homogeneity_test + +.. autoclass:: diff_diff.StuteJointResult + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/choosing_estimator.rst b/docs/choosing_estimator.rst index 155a6bf0..796864be 100644 --- a/docs/choosing_estimator.rst +++ b/docs/choosing_estimator.rst @@ -93,6 +93,10 @@ Quick Reference - Continuous dose / treatment intensity - Strong Parallel Trends (SPT) for dose-response; PT for binarized ATT - ATT\ :sup:`loc` (PT); ATT(d), ACRT(d) (SPT) + * - ``HeterogeneousAdoptionDiD`` + - Universal rollout, dose varies, no untreated unit + - dCDH 2026 Assumptions (Design 1' QUG case or Design 1 with A6/A5) + - WAS or WAS\ :sub:`d_lower` per resolved estimand; event-study Appendix B.2 * - ``SunAbraham`` - Staggered adoption, interaction-weighted - Conditional parallel trends @@ -357,6 +361,49 @@ Use :class:`~diff_diff.ContinuousDiD` when: print(f"Overall ATT: {results.overall_att:.3f}") att_curve = results.dose_response_att.to_dataframe() +Universal Rollout / No Untreated Control +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Use :class:`~diff_diff.HeterogeneousAdoptionDiD` when: + +- **Every unit is treated at the post period** (universal-rollout policy, + industry-wide tariff change, simultaneous launch into all markets) +- Treatment **intensity (dose) varies across units**, but no genuinely + untreated control group exists to anchor a standard DiD contrast +- :class:`~diff_diff.ContinuousDiD` is unavailable because its untreated-group + requirement (``D = 0``) is violated + +The estimator implements de Chaisemartin, Ciccia, D'Haultfoeuille and Knau +(2026, arXiv:2405.04465v6) and resolves to one of two estimands depending on +the dose support: + +- **Design 1' (QUG case, ``d_lower = 0``)** identifies the **Weighted Average + Slope (WAS)** under the Quasi-Untreated-Group assumption (units with the + smallest dose serve as the comparison anchor). The shipped result class + exposes ``target_parameter == "WAS"``. +- **Design 1 (no QUG, ``d_lower > 0``)** identifies ``WAS_{d_lower}`` under + Assumption 6, or sign identification only under Assumption 5; neither + additional assumption is testable via pre-trends. Result class exposes + ``target_parameter == "WAS_d_lower"``. + +The dose-distribution path is auto-detected. Run +:func:`~diff_diff.did_had_pretest_workflow` to vet the identifying assumptions +before estimation; see :doc:`api/had` for the full API and SE-regime contract. + +.. code-block:: python + + from diff_diff import HeterogeneousAdoptionDiD, did_had_pretest_workflow + + pretests = did_had_pretest_workflow(data, outcome_col='y', unit_col='unit', + time_col='period', dose_col='dose') + + est = HeterogeneousAdoptionDiD() + results = est.fit(data, outcome_col='y', unit_col='unit', + time_col='period', dose_col='dose') + + print(f"Resolved estimand: {results.target_parameter}") + print(f"Estimate: {results.att:.3f}") + Efficient DiD ~~~~~~~~~~~~~ @@ -615,6 +662,9 @@ differences helps interpret results and choose appropriate inference. * - ``ContinuousDiD`` - Analytical (influence function) - Uses influence-function-based SEs by default. Use ``n_bootstrap=199`` (or higher) for multiplier bootstrap inference with proper CIs. + * - ``HeterogeneousAdoptionDiD`` + - Path-dependent (CCT-2014 / 2SLS / Binder TSL) + - Three SE regimes per :doc:`api/had`. **Unweighted**: continuous-dose paths use the CCT-2014 weighted-robust SE from the in-house ``lprobust`` port; mass-point uses a 2SLS sandwich. **Deprecated ``weights=`` shortcut**: continuous reuses CCT-2014; mass-point uses analytical weighted 2SLS (``classical`` / ``hc1``; CR1 when ``cluster=`` is supplied, except mass-point + ``cluster=`` + ``aggregate="event_study"`` + ``cband=True`` is rejected outright - see :doc:`api/had` for the cluster-combination deviation note); yields ``variance_formula="pweight"`` / ``"pweight_2sls"``. **``survey_design=SurveyDesign(weights="col", ...)``**: both paths compose Binder (1983) Taylor-series linearization (``"survey_binder_tsl"`` / ``"survey_binder_tsl_2sls"``); mass-point + ``survey_design=`` + ``cluster=`` is also rejected outright (combined survey + cluster inference is deferred). The two weighted families differ on this estimator until the next-minor unification lands. Per-horizon CIs are pointwise; sup-t bands available only on the weighted event-study path via ``cband=True``. * - ``SunAbraham`` - Cluster-robust (unit level) - Clusters at unit level by default. Specify ``cluster`` to override. Use ``n_bootstrap`` for pairs bootstrap inference. @@ -777,6 +827,11 @@ estimation. The depth of support varies by estimator: - Full - Full (analytical) - Multiplier at PSU + * - ``HeterogeneousAdoptionDiD`` + - pweight only + - Full (Binder TSL) + - -- + - Multiplier (event-study, ``cband=True`` only) * - ``EfficientDiD`` - Full - Full diff --git a/docs/doc-deps.yaml b/docs/doc-deps.yaml index 2a5e6f4f..1e5a34dc 100644 --- a/docs/doc-deps.yaml +++ b/docs/doc-deps.yaml @@ -385,6 +385,19 @@ sources: diff_diff/had_pretests.py: drift_risk: medium + docs: + - path: docs/methodology/REGISTRY.md + section: "HeterogeneousAdoptionDiD" + type: methodology + - path: docs/api/had.rst + section: "HAD Pretests" + type: api_reference + - path: diff_diff/guides/llms.txt + section: "Estimators" + type: user_guide + + diff_diff/local_linear.py: + drift_risk: low docs: - path: docs/methodology/REGISTRY.md section: "HeterogeneousAdoptionDiD" diff --git a/docs/practitioner_decision_tree.rst b/docs/practitioner_decision_tree.rst index 6b0f4bfd..14bf4883 100644 --- a/docs/practitioner_decision_tree.rst +++ b/docs/practitioner_decision_tree.rst @@ -44,6 +44,11 @@ Which of these best describes your situation? Your outcome comes from a survey with complex sampling. Go to :ref:`section-survey`. +7. **All my markets received the campaign at the same time, but spend levels varied** (no untreated control market exists) + + Universal rollout with dose-only variation. Go to + :ref:`section-no-untreated`. + .. tip:: In academic literature, "rolling out in waves" is called *staggered adoption*, @@ -258,6 +263,54 @@ appropriate identification assumptions in place. require Strong Parallel Trends (see warning above). +.. _section-no-untreated: + +Universal Rollout (No Untreated Markets) +---------------------------------------- + +**Your situation:** Every market got the campaign at the same time - there is no +holdout group - but spending levels varied across markets. ``ContinuousDiD`` cannot +help here because it requires an untreated comparison group; standard DiD has no +control to anchor the contrast. + +**Recommended method:** :class:`~diff_diff.HeterogeneousAdoptionDiD` + +This estimator implements de Chaisemartin, Ciccia, D'Haultfoeuille and Knau (2026) +and resolves to one of two estimands depending on whether the smallest-dose +markets can serve as a quasi-untreated anchor (Design 1') or whether the +identification rests on stronger structural assumptions (Design 1). + +.. code-block:: python + + from diff_diff import HeterogeneousAdoptionDiD, did_had_pretest_workflow + + # Run the pretest battery first - it surfaces violations of the HAD + # identification assumptions (it does NOT pick the design path; the + # estimator does that internally from the dose support). + pretests = did_had_pretest_workflow( + data, outcome_col="y", unit_col="unit", + time_col="period", dose_col="dose", + ) + print(pretests) + + est = HeterogeneousAdoptionDiD() + results = est.fit( + data, outcome_col="y", unit_col="unit", + time_col="period", dose_col="dose", + ) + print(f"Resolved estimand: {results.target_parameter}") + print(f"Average lift per unit of dose: {results.att:.2f}") + +.. note:: + + **Academic term:** The estimator targets the *Weighted Average Slope (WAS)* under + the QUG / Design 1' case, or *WAS_{d_lower}* under Design 1. Neither identifying + assumption is testable via pre-trends alone - run + :func:`~diff_diff.did_had_pretest_workflow` for the recommended battery. See + :doc:`api/had` for the inference contract (three SE regimes; pointwise CIs; + sup-t bands only on the weighted event-study path). + + .. _section-few-markets: Few Test Markets @@ -377,6 +430,9 @@ At a Glance * - Varied spending levels - ``ContinuousDiD`` - Dose-response curve + * - Universal rollout, no untreated markets + - ``HeterogeneousAdoptionDiD`` + - Targets WAS / WAS_{d_lower} when no holdout exists * - Only a few test markets - ``SyntheticDiD`` - Optimal with few treated units diff --git a/docs/r_comparison.rst b/docs/r_comparison.rst index f8507d2b..9a641691 100644 --- a/docs/r_comparison.rst +++ b/docs/r_comparison.rst @@ -213,6 +213,36 @@ The synthdid package implements Arkhangelsky et al. (2021): post_periods=post_periods ) +Heterogeneous Adoption (HAD) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When every unit is treated at the post period (universal-rollout policies, +industry-wide regime changes) but treatment intensity varies across units, +the standard R workhorses (``did``, ``fixest``, ``synthdid``, +``DIDmultiplegtDYN``) assume an untreated comparison group exists and do +not apply. The dedicated R package ``DIDHAD`` (de Chaisemartin et al., +August 2025) covers the QUG case (Design 1', ``d_lower = 0``) from the +same arXiv paper. + +``diff-diff`` ships :class:`~diff_diff.HeterogeneousAdoptionDiD`, which +implements de Chaisemartin, Ciccia, D'Haultfoeuille and Knau (2026, +arXiv:2405.04465v6) and adds two surfaces beyond the QUG-focused R +package: Design 1 (no QUG, ``d_lower > 0``, targets ``WAS_{d_lower}`` under +Assumption 6 or sign-only under Assumption 5), and survey-design +integration via Binder (1983) Taylor-series linearization (sampling weights ++ optional strata / PSU / FPC). The diagnostic battery +:func:`~diff_diff.did_had_pretest_workflow` surfaces violations of the HAD +identification assumptions (the design path is auto-detected separately by +:meth:`HeterogeneousAdoptionDiD.fit` from the dose support). + +.. code-block:: python + + from diff_diff import HeterogeneousAdoptionDiD + + est = HeterogeneousAdoptionDiD() + results = est.fit(data, outcome_col='y', unit_col='unit', + time_col='period', dose_col='dose') + Key Differences --------------- @@ -372,6 +402,11 @@ Feature Comparison Table - ❌ - ❌ - ❌ + * - Heterogeneous adoption (HAD) + - ✅ + - ❌ + - ❌ + - ❌ .. note:: @@ -383,6 +418,10 @@ Feature Comparison Table Continuous DiD is available via the ``did`` package continuous extension; Triple Difference requires manual implementation in R. TROP and Efficient DiD have no direct R equivalents. + HeterogeneousAdoptionDiD (dCDH 2026) overlaps with the dedicated R + package ``DIDHAD`` (de Chaisemartin et al., 2025), which covers the + QUG case (Design 1'); diff-diff additionally covers Design 1 (no QUG, + ``WAS_{d_lower}``) and survey-design integration via Binder TSL. Migration Tips -------------- @@ -399,3 +438,9 @@ Migration Tips 5. **Missing data**: diff-diff requires complete data; use ``balance_panel()`` or ``dropna()`` first + +6. **Heterogeneous Adoption (HAD)**: If you need surfaces the R ``DIDHAD`` + package does not cover - Design 1 (no QUG, ``WAS_{d_lower}``) or + survey-design integration - reach for + :class:`~diff_diff.HeterogeneousAdoptionDiD`. See the + `Heterogeneous Adoption (HAD)`_ section above for the migration pattern. diff --git a/docs/references.rst b/docs/references.rst index 7a5cda6d..33a6f2f8 100644 --- a/docs/references.rst +++ b/docs/references.rst @@ -66,7 +66,7 @@ Survey-Design Inference (Taylor-Series Linearization) - **Binder, D. A. (1983).** "On the Variances of Asymptotically Normal Estimators from Complex Surveys." *International Statistical Review*, 51(3), 279-292. https://doi.org/10.2307/1402588 - Foundational TSL (Taylor-Series Linearization) variance derivation used across diff-diff's survey-aware estimators (``compute_survey_if_variance`` and the per-estimator influence-function compositions, including the dCDH and HeterogeneousAdoptionDiD ``survey=`` paths). + Foundational TSL (Taylor-Series Linearization) variance derivation used across diff-diff's survey-aware estimators (``compute_survey_if_variance`` and the per-estimator influence-function compositions, including the dCDH and HeterogeneousAdoptionDiD ``survey_design=`` paths). Placebo Tests and DiD Diagnostics --------------------------------- diff --git a/docs/troubleshooting.rst b/docs/troubleshooting.rst index cdb57f12..d438e36c 100644 --- a/docs/troubleshooting.rst +++ b/docs/troubleshooting.rst @@ -474,6 +474,142 @@ cannot produce an ATT estimate. post_obs = group[group['period'] >= g] print(f"Cohort {g}: {len(post_obs)} post-treatment observations") +HeterogeneousAdoptionDiD (HAD) Issues +------------------------------------- + +"Resolved estimand is not what I expected (WAS vs WAS_d_lower)" +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**Problem:** ``HeterogeneousAdoptionDiD`` resolves ``target_parameter`` to +``"WAS_d_lower"`` when you expected ``"WAS"`` (or vice versa). + +**Cause:** HAD auto-detects the design path from the dose distribution. The +``_detect_design`` rule resolves to Design 1' (``continuous_at_zero``, +targets WAS) when EITHER ``d.min() == 0`` exactly OR ``d.min()`` is a small +positive value below ``0.01 * median(|d|)`` (the small-share-of-treated +escape clause). Otherwise (``d.min()`` larger than that threshold) the +estimator routes to Design 1, with a further check for mass-point structure +(modal fraction at ``d.min()`` exceeding 2% routes to ``mass_point``; +otherwise ``continuous_near_d_lower``); both Design 1 paths target +``WAS_{d_lower}``. So a Design 1 resolution only fires when ``d.min()`` +is meaningfully positive relative to the dose scale. + +**Solutions:** + +.. code-block:: python + + # Inspect the dose support before fitting + import numpy as np + d = data['dose'].to_numpy() + print(data['dose'].describe()) + print(f"d.min() = {d.min():.6g}; " + f"0.01 * median(|d|) = {0.01 * np.median(np.abs(d)):.6g}; " + f"d.min() < threshold => Design 1' (WAS)") + + # Check the resolved estimand after fitting + results = est.fit(data, outcome_col='y', unit_col='unit', + time_col='period', dose_col='dose') + print(f"Resolved: {results.target_parameter}") + + # If you intend Design 1' but `d.min()` exceeds the threshold, verify + # the dose-variable encoding (e.g. log-transformed doses where 0 was + # mapped to a small positive value larger than 1% of the median). + +"Mass-point design selected" +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**Problem:** HAD reports that the ``mass_point`` design was selected +instead of ``continuous_at_zero`` or ``continuous_near_d_lower``. + +**Cause:** ``mass_point`` is a distinct Design 1 estimator path from the +dCDH 2026 paper (Section 3.2.4), not a fallback from the continuous +local-linear fits. ``_detect_design()`` resolves to ``mass_point`` when the +modal fraction at ``d.min()`` exceeds 2%, signalling a heavy point mass at +the dose-support boundary. On this path both the point estimate and the SE +differ from the continuous paths: the estimator uses the Wald-IV +sample-average ratio with binary instrument ``Z_g = 1{D_{g,2} > d_lower}`` +- ``(Ybar_{Z=1} - Ybar_{Z=0}) / (Dbar_{Z=1} - Dbar_{Z=0})`` - and inference +uses the structural-residual 2SLS sandwich (the local-linear / CCT-2014 +SE path is not used here). + +**Solutions:** + +.. code-block:: python + + # Inspect the resolved design + print(f"Design: {results.design}") # 'mass_point' here + + # The mass-point Wald-IV estimator + structural-residual 2SLS + # sandwich is the canonical Section 3.2.4 path for designs with a + # heavy boundary point mass; accept the resolution unless you can + # re-bin the dose variable so the modal fraction at d.min() drops + # below 2% (then the detector picks continuous_near_d_lower). + +"NotImplementedError on survey + mass-point + vcov_type='classical'" +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**Problem:** Calling ``HeterogeneousAdoptionDiD.fit(..., vcov_type="classical")`` +under ``survey_design=SurveyDesign(...)`` (or under the deprecated ``survey=`` +alias) raises ``NotImplementedError`` on the mass-point path. The same +``NotImplementedError`` fires on the deprecated ``weights=`` shortcut + +``aggregate="event_study"`` + ``cband=True``. + +**Cause:** The per-unit 2SLS influence function returned by the mass-point fit +is HC1-scaled so that ``compute_survey_if_variance`` and the sup-t bootstrap +target ``V_HC1`` consistently. Mixing it with a classical analytical SE would +silently report a ``V_HC1``-targeted variance under a ``classical`` label. + +**Solutions:** + +.. code-block:: python + + # The constructor default `robust=False` maps to `vcov_type='classical'` + # and triggers the guard on the mass-point survey path - so plain + # `HeterogeneousAdoptionDiD()` is NOT a workaround. Pick one of: + est = HeterogeneousAdoptionDiD(vcov_type='hc1') + # Or equivalently: + est = HeterogeneousAdoptionDiD(robust=True) # maps to vcov_type='hc1' + +A classical-aligned IF derivation is queued for a follow-up release; until +then, ``vcov_type='hc1'`` (or the equivalent ``robust=True``) is the +recommended path for survey + mass-point fits. See :doc:`api/had` for the +full SE-regime contract. + +"Panel-only event-study restriction" +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**Problem:** ``HeterogeneousAdoptionDiD.fit(..., aggregate="event_study")`` +raises on a staggered panel. + +**Cause:** The Appendix B.2 event-study extension requires either a +common-adoption panel (single first-treat period; ``first_treat_col`` is +then optional and the period is inferred from the dose invariant) or a +staggered panel with ``first_treat_col`` provided so the estimator can +auto-filter to the last-treatment cohort plus never-treated units (with +a ``UserWarning``). The fit raises only when the panel is staggered +**and** ``first_treat_col`` is missing. + +**Solutions:** + +.. code-block:: python + + # Primary remedy: pass `first_treat_col` so the estimator auto-filters + # to the last-treatment cohort + never-treated and emits a UserWarning. + est = HeterogeneousAdoptionDiD() + results = est.fit(data, outcome_col='y', unit_col='unit', + time_col='period', dose_col='dose', + first_treat_col='first_treat', + aggregate='event_study') + + # Equivalent: subset to the last-treatment cohort + never-treated + # before fitting (skips the UserWarning). + last_cohort = data['first_treat'].max() + subset = data[(data['first_treat'] == last_cohort) | + (data['first_treat'] == 0)] + results = est.fit(subset, outcome_col='y', unit_col='unit', + time_col='period', dose_col='dose', + aggregate='event_study') + Imputation / Two-Stage DiD Issues ----------------------------------