Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions diff_diff/bacon.py
Original file line number Diff line number Diff line change
Expand Up @@ -1085,6 +1085,7 @@ def bacon_decompose(
Use 0 (or np.inf) for never-treated units.
weights : str, default="approximate"
Weight calculation method:

- "approximate": Fast simplified formula (default). Good for
diagnostic purposes where relative weights are sufficient.
- "exact": Variance-based weights from Goodman-Bacon (2021)
Expand All @@ -1094,6 +1095,7 @@ def bacon_decompose(
-------
BaconDecompositionResults
Object containing decomposition results with:

- twfe_estimate: The overall TWFE coefficient
- comparisons: List of all 2x2 comparisons with estimates and weights
- Weight totals by comparison type
Expand Down
2 changes: 1 addition & 1 deletion diff_diff/chaisemartin_dhaultfoeuille_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -701,7 +701,7 @@ def __repr__(self) -> str:

@property
def coef_var(self) -> float:
"""SE / |DID_M|; NaN when DID_M is 0 or SE non-finite."""
"""SE / abs(DID_M); NaN when DID_M is 0 or SE non-finite."""
if not (np.isfinite(self.overall_se) and self.overall_se >= 0):
return np.nan
if not np.isfinite(self.overall_att) or self.overall_att == 0:
Expand Down
2 changes: 1 addition & 1 deletion diff_diff/continuous_did_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ def __repr__(self) -> str:

@property
def coef_var(self) -> float:
"""Coefficient of variation: SE / |overall ATT|. NaN when ATT is 0 or SE non-finite."""
"""Coefficient of variation: SE / abs(overall ATT). NaN when ATT is 0 or SE non-finite."""
if not (np.isfinite(self.overall_att_se) and self.overall_att_se >= 0):
return np.nan
if not np.isfinite(self.overall_att) or self.overall_att == 0:
Expand Down
5 changes: 2 additions & 3 deletions diff_diff/diagnostic_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,10 +256,9 @@ class DiagnosticReport:
Column names identifying the panel structure.
pre_periods, post_periods : list, optional
Explicit pre- and post-treatment period labels.
run_parallel_trends, run_sensitivity, run_placebo, run_bacon,
run_design_effect, run_heterogeneity, run_epv, run_pretrends_power : bool
run_parallel_trends, run_sensitivity, run_placebo, run_bacon, run_design_effect, run_heterogeneity, run_epv, run_pretrends_power : bool
Per-check opt-in flags. ``run_placebo`` defaults to ``False`` (opt-in,
expensive, currently not implemented placebo key remains reserved
expensive, currently not implemented - placebo key remains reserved
as ``skipped`` in the schema). All other checks default to ``True``
and are further gated by estimator-type and instance-level
applicability (see ``docs/methodology/REPORTING.md``).
Expand Down
2 changes: 1 addition & 1 deletion diff_diff/efficient_did_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ def __repr__(self) -> str:

@property
def coef_var(self) -> float:
"""Coefficient of variation: SE / |overall ATT|. NaN when ATT is 0 or SE non-finite."""
"""Coefficient of variation: SE / abs(overall ATT). NaN when ATT is 0 or SE non-finite."""
if not (np.isfinite(self.overall_se) and self.overall_se >= 0):
return np.nan
if not np.isfinite(self.overall_att) or self.overall_att == 0:
Expand Down
2 changes: 2 additions & 0 deletions diff_diff/had.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ class HeterogeneousAdoptionDiDResults:
``(Ybar_{Z=1} - Ybar_{Z=0}) / (Dbar_{Z=1} - Dbar_{Z=0})``.
se : float
Standard error on the beta-scale. For continuous designs:

- Unweighted or ``weights=<array>``: CCT-2014 weighted-robust SE
from Phase 1c divided by ``|den|`` (``den`` = raw or weighted
denominator depending on fit path).
Expand All @@ -241,6 +242,7 @@ class HeterogeneousAdoptionDiDResults:
aligned with ``tau_bc``) routed through
:func:`compute_survey_if_variance` for PSU-aggregated,
FPC/strata-adjusted variance, divided by ``|den|``.

In both cases the higher-order variance from ``mean(ΔY)`` is
dominated by the nonparametric boundary estimate in large
samples and is not included in the leading-order formula. For
Expand Down
15 changes: 11 additions & 4 deletions diff_diff/honest_did.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@ class DeltaSD:
Smoothness restriction on trend violations (Delta^{SD}).

Restricts the second differences of the trend violations:
|delta_{t+1} - 2*delta_t + delta_{t-1}| <= M

.. math::

|\\delta_{t+1} - 2\\delta_t + \\delta_{t-1}| \\le M

When M=0, this enforces that violations follow a linear trend
(linear extrapolation of pre-trends). Larger M allows more
Expand Down Expand Up @@ -75,7 +78,10 @@ class DeltaRM:

Post-treatment consecutive first differences are bounded by Mbar
times the maximum pre-treatment first difference:
|delta_{t+1} - delta_t| <= Mbar * max_{s<0} |delta_{s+1} - delta_s|

.. math::

|\\delta_{t+1} - \\delta_t| \\le \\overline{M} \\cdot \\max_{s<0} |\\delta_{s+1} - \\delta_s|

When Mbar=0, this enforces zero post-treatment first differences.
Mbar=1 means post-period first differences can be as large as the
Expand Down Expand Up @@ -109,8 +115,9 @@ class DeltaSDRM:
Combined smoothness and relative magnitudes restriction.

Imposes both:
1. Smoothness: |delta_{t+1} - 2*delta_t + delta_{t-1}| <= M
2. Relative magnitudes: |delta_{t+1} - delta_t| <= Mbar * max_{s<0} |delta_{s+1} - delta_s|

1. Smoothness: :math:`|\\delta_{t+1} - 2\\delta_t + \\delta_{t-1}| \\le M`
2. Relative magnitudes: :math:`|\\delta_{t+1} - \\delta_t| \\le \\overline{M} \\cdot \\max_{s<0} |\\delta_{s+1} - \\delta_s|`

This is more restrictive than either constraint alone.

Expand Down
2 changes: 1 addition & 1 deletion diff_diff/imputation.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ class ImputationDiD(ImputationDiDBootstrapMixin):
- "silent": Drop columns silently
horizon_max : int, optional
Maximum event-study horizon. If set, event study effects are only
computed for |h| <= horizon_max.
computed for abs(h) <= horizon_max.
aux_partition : str, default="cohort_horizon"
Controls the auxiliary model partition for Theorem 3 variance:
- "cohort_horizon": Groups by cohort x relative time (tightest SEs)
Expand Down
6 changes: 3 additions & 3 deletions diff_diff/imputation_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,9 @@ class ImputationDiDResults:
n_obs : int
Total number of observations.
n_treated_obs : int
Number of treated observations (|Omega_1|).
Number of treated observations (:math:`|\\Omega_1|`).
n_untreated_obs : int
Number of untreated observations (|Omega_0|).
Number of untreated observations (:math:`|\\Omega_0|`).
n_treated_units : int
Number of ever-treated units.
n_control_units : int
Expand Down Expand Up @@ -155,7 +155,7 @@ def __repr__(self) -> str:

@property
def coef_var(self) -> float:
"""Coefficient of variation: SE / |overall ATT|. NaN when ATT is 0 or SE non-finite."""
"""Coefficient of variation: SE / abs(overall ATT). NaN when ATT is 0 or SE non-finite."""
if not (np.isfinite(self.overall_se) and self.overall_se >= 0):
return np.nan
if not np.isfinite(self.overall_att) or self.overall_att == 0:
Expand Down
2 changes: 2 additions & 0 deletions diff_diff/prep.py
Original file line number Diff line number Diff line change
Expand Up @@ -834,6 +834,7 @@ def rank_control_units(
-------
pd.DataFrame
Ranked control units with columns:

- unit: Unit identifier
- quality_score: Combined quality score (0-1, higher is better)
- outcome_trend_score: Pre-treatment outcome trend similarity
Expand All @@ -846,6 +847,7 @@ def rank_control_units(
- is_required: Whether unit was in require_units

If suggest_treatment_candidates=True (and no treated units):

- unit: Unit identifier
- treatment_candidate_score: Suitability as treatment unit
- avg_outcome_level: Pre-treatment outcome mean
Expand Down
20 changes: 12 additions & 8 deletions diff_diff/results.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ def __repr__(self) -> str:

@property
def coef_var(self) -> float:
"""Coefficient of variation: SE / |ATT|. NaN when ATT is 0 or SE non-finite."""
"""Coefficient of variation: SE / abs(ATT). NaN when ATT is 0 or SE non-finite."""
if not (np.isfinite(self.se) and self.se >= 0):
return np.nan
if not np.isfinite(self.att) or self.att == 0:
Expand Down Expand Up @@ -468,7 +468,7 @@ def post_period_effects(self) -> Dict[Any, PeriodEffect]:

@property
def coef_var(self) -> float:
"""Coefficient of variation: SE / |overall ATT|. NaN when ATT is 0 or SE non-finite."""
"""Coefficient of variation: SE / abs(overall ATT). NaN when ATT is 0 or SE non-finite."""
if not (np.isfinite(self.avg_se) and self.avg_se >= 0):
return np.nan
if not np.isfinite(self.avg_att) or self.avg_att == 0:
Expand Down Expand Up @@ -919,7 +919,7 @@ def __getstate__(self) -> Dict[str, Any]:

@property
def coef_var(self) -> float:
"""Coefficient of variation: SE / |ATT|. NaN when ATT is 0 or SE non-finite."""
"""Coefficient of variation: SE / abs(ATT). NaN when ATT is 0 or SE non-finite."""
if not (np.isfinite(self.se) and self.se >= 0):
return np.nan
if not np.isfinite(self.att) or self.att == 0:
Expand Down Expand Up @@ -1114,14 +1114,16 @@ def get_loo_effects_df(self) -> pd.DataFrame:
full-design survey jackknife path, which uses PSU-level LOO).

Available on:

* non-survey jackknife fits (classical Arkhangelsky Algorithm 3).
* pweight-only survey jackknife fits (Algorithm 3 with post-hoc
ω_eff composition; PSU labels in ``survey_metadata`` come from
implicit-PSU metadata but the LOO remains unit-level).

Blocked on:

* full-design survey jackknife fits (strata / PSU / FPC set in
``SurveyDesign``) the underlying replicates are PSU-level
``SurveyDesign``) - the underlying replicates are PSU-level
``τ̂_{(h,j)}`` (Rust & Rao 1996), not unit-level. See
``result.placebo_effects`` for the raw PSU-level replicate
array and REGISTRY §SyntheticDiD "Note (survey + jackknife
Expand All @@ -1142,10 +1144,12 @@ def get_loo_effects_df(self) -> pd.DataFrame:
-------
pd.DataFrame
Columns:
- ``unit`` — user's unit ID
- ``role`` — ``'control'`` or ``'treated'``
- ``att_loo`` — ATT with this unit dropped
- ``delta_from_full`` — ``att_loo - self.att``

- ``unit`` - user's unit ID
- ``role`` - ``'control'`` or ``'treated'``
- ``att_loo`` - ATT with this unit dropped
- ``delta_from_full`` - ``att_loo - self.att``

Sorted by ``|delta_from_full|`` descending, NaN rows at the end.
"""
if self.variance_method != "jackknife":
Expand Down
2 changes: 1 addition & 1 deletion diff_diff/stacked_did_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ def __repr__(self) -> str:

@property
def coef_var(self) -> float:
"""Coefficient of variation: SE / |overall ATT|. NaN when ATT is 0 or SE non-finite."""
"""Coefficient of variation: SE / abs(overall ATT). NaN when ATT is 0 or SE non-finite."""
if not (np.isfinite(self.overall_se) and self.overall_se >= 0):
return np.nan
if not np.isfinite(self.overall_att) or self.overall_att == 0:
Expand Down
5 changes: 5 additions & 0 deletions diff_diff/staggered.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,16 +175,19 @@ class CallawaySantAnna(
Random seed for reproducibility.
rank_deficient_action : str, default="warn"
Action when design matrix is rank-deficient (linearly dependent columns):

- "warn": Issue warning and drop linearly dependent columns (default)
- "error": Raise ValueError
- "silent": Drop columns silently without warning
base_period : str, default="varying"
Method for selecting the base (reference) period for computing
ATT(g,t). Options:

- "varying": For pre-treatment periods (t < g - anticipation), use
t-1 as base (consecutive comparisons). For post-treatment, use
g-1-anticipation. Requires t-1 to exist in data.
- "universal": Always use g-1-anticipation as base period.

Both produce identical post-treatment effects. Matches R's
did::att_gt() base_period parameter.
cband : bool, default=True
Expand Down Expand Up @@ -217,12 +220,14 @@ class CallawaySantAnna(
pscore_fallback : str, default="error"
Action when propensity score estimation fails entirely
(``LinAlgError`` or ``ValueError`` from IRLS):

- "error": Raise the exception (default). Ensures the user is
aware of estimation failures.
- "unconditional": Fall back to unconditional propensity
with a warning. For IPW, this drops all covariates. For DR,
the propensity model becomes unconditional but outcome
regression still uses covariates.

When ``rank_deficient_action="error"``, errors are always
re-raised regardless of this setting.

Expand Down
2 changes: 1 addition & 1 deletion diff_diff/staggered_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ def __repr__(self) -> str:

@property
def coef_var(self) -> float:
"""Coefficient of variation: SE / |overall ATT|. NaN when ATT is 0 or SE non-finite."""
"""Coefficient of variation: SE / abs(overall ATT). NaN when ATT is 0 or SE non-finite."""
if not (np.isfinite(self.overall_se) and self.overall_se >= 0):
return np.nan
if not np.isfinite(self.overall_att) or self.overall_att == 0:
Expand Down
2 changes: 1 addition & 1 deletion diff_diff/staggered_triple_diff_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ def __repr__(self) -> str:

@property
def coef_var(self) -> float:
"""Coefficient of variation: SE / |overall ATT|. NaN when ATT is 0 or SE non-finite."""
"""Coefficient of variation: SE / abs(overall ATT). NaN when ATT is 0 or SE non-finite."""
if not (np.isfinite(self.overall_se) and self.overall_se >= 0):
return np.nan
if not np.isfinite(self.overall_att) or self.overall_att == 0:
Expand Down
2 changes: 1 addition & 1 deletion diff_diff/sun_abraham.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ def __repr__(self) -> str:

@property
def coef_var(self) -> float:
"""Coefficient of variation: SE / |overall ATT|. NaN when ATT is 0 or SE non-finite."""
"""Coefficient of variation: SE / abs(overall ATT). NaN when ATT is 0 or SE non-finite."""
if not (np.isfinite(self.overall_se) and self.overall_se >= 0):
return np.nan
if not np.isfinite(self.overall_att) or self.overall_att == 0:
Expand Down
2 changes: 2 additions & 0 deletions diff_diff/synthetic_did.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class SyntheticDiD(DifferenceInDifferences):
pre-treatment trends.

This method is particularly useful when:

- You have few treated units (possibly just one)
- Parallel trends assumption may be questionable
- Control units are heterogeneous and need reweighting
Expand All @@ -52,6 +53,7 @@ class SyntheticDiD(DifferenceInDifferences):
Significance level for confidence intervals.
variance_method : str, default="placebo"
Method for variance estimation:

- "placebo": Placebo-based variance matching R's synthdid::vcov(method="placebo").
Implements Algorithm 4 from Arkhangelsky et al. (2021). Library default
(R's default is ``"bootstrap"``; we default to placebo because it is
Expand Down
4 changes: 4 additions & 0 deletions diff_diff/triple_diff.py
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,7 @@ class TripleDifference:
----------
estimation_method : str, default="dr"
Estimation method to use:

- "dr": Doubly robust (recommended). Consistent if either the outcome
model or propensity score model is correctly specified.
- "reg": Regression adjustment (outcome regression).
Expand All @@ -385,6 +386,7 @@ class TripleDifference:
or above (1 - pscore_trim) are clipped to avoid extreme weights.
rank_deficient_action : str, default="warn"
Action when design matrix is rank-deficient (linearly dependent columns):

- "warn": Issue warning and drop linearly dependent columns (default)
- "error": Raise ValueError
- "silent": Drop columns silently without warning
Expand All @@ -397,11 +399,13 @@ class TripleDifference:
(1996). Only applies to IPW and DR estimation methods.
pscore_fallback : str, default="error"
Action when propensity score estimation fails:

- "error": Raise the exception (default)
- "unconditional": Fall back to unconditional propensity with
a warning. For IPW, drops all covariates. For DR, the
propensity model becomes unconditional but outcome regression
still uses covariates.

When ``rank_deficient_action="error"``, errors are always
re-raised regardless of this setting.

Expand Down
2 changes: 1 addition & 1 deletion diff_diff/trop.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ class TROP(TROPLocalMixin, TROPGlobalMixin):
2. **Exponential distance-based unit weights**: ω_j = exp(-λ_unit × d(j,i))
where d(j,i) is the RMSE of outcome differences between units

3. **Exponential time decay weights**: θ_s = exp(-λ_time × |s-t|)
3. **Exponential time decay weights**: θ_s = exp(-λ_time × :math:`|s-t|`)
weighting pre-treatment periods by proximity to treatment

Tuning parameters (λ_time, λ_unit, λ_nn) are selected via leave-one-out
Expand Down
2 changes: 1 addition & 1 deletion diff_diff/trop_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ def __repr__(self) -> str:

@property
def coef_var(self) -> float:
"""Coefficient of variation: SE / |ATT|. NaN when ATT is 0 or SE non-finite."""
"""Coefficient of variation: SE / abs(ATT). NaN when ATT is 0 or SE non-finite."""
if not (np.isfinite(self.se) and self.se >= 0):
return np.nan
if not np.isfinite(self.att) or self.att == 0:
Expand Down
1 change: 1 addition & 0 deletions diff_diff/twfe.py
Original file line number Diff line number Diff line change
Expand Up @@ -602,6 +602,7 @@ def decompose(
Use 0 (or np.inf) for never-treated units.
weights : str, default="approximate"
Weight calculation method:

- "approximate": Fast simplified formula (default). Good for
diagnostic purposes where relative weights are sufficient.
- "exact": Variance-based weights from Goodman-Bacon (2021)
Expand Down
2 changes: 1 addition & 1 deletion diff_diff/two_stage.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ class TwoStageDiD(TwoStageDiDBootstrapMixin):
- "silent": Drop columns silently
horizon_max : int, optional
Maximum event-study horizon. If set, event study effects are only
computed for |h| <= horizon_max.
computed for abs(h) <= horizon_max.
pretrends : bool, default=False
If True, event study includes pre-treatment horizons for visual
pre-trends assessment. Pre-period effects should be ~0 under
Expand Down
2 changes: 1 addition & 1 deletion diff_diff/two_stage_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ def __repr__(self) -> str:

@property
def coef_var(self) -> float:
"""Coefficient of variation: SE / |overall ATT|. NaN when ATT is 0 or SE non-finite."""
"""Coefficient of variation: SE / abs(overall ATT). NaN when ATT is 0 or SE non-finite."""
if not (np.isfinite(self.overall_se) and self.overall_se >= 0):
return np.nan
if not np.isfinite(self.overall_att) or self.overall_att == 0:
Expand Down
12 changes: 4 additions & 8 deletions docs/api/local_linear.rst
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,10 @@ to ``int_0^1 k(u) du = 1/2``; the uniform kernel uses

.. autofunction:: diff_diff.uniform_kernel

.. autodata:: diff_diff.KERNELS
:annotation: : dict[str, Callable[[np.ndarray], np.ndarray]]
:no-value:

Mapping from kernel name (``"epanechnikov"`` / ``"triangular"`` /
``"uniform"``) to its callable evaluator on ``[0, 1]``. Pass the name
string (not the callable) to ``local_linear_fit`` and
``mse_optimal_bandwidth`` via their ``kernel=`` argument.
.. py:data:: diff_diff.KERNELS
:type: dict[str, Callable[[np.ndarray], np.ndarray]]

Mapping from kernel name (``"epanechnikov"`` / ``"triangular"`` / ``"uniform"``) to its callable evaluator on ``[0, 1]``. Pass the name string (not the callable) to ``local_linear_fit`` and ``mse_optimal_bandwidth`` via their ``kernel=`` argument.

.. autofunction:: diff_diff.kernel_moments

Expand Down
Loading
Loading