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
12 changes: 12 additions & 0 deletions diff_diff/guides/llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -967,6 +967,18 @@ results = bacon_decompose(data, outcome='y', unit='id', time='t', first_treat='f

## Results Objects

**Flat-alias compatibility note.** Every staggered result class in this
section (those with canonical `overall_*` / `overall_att_*` / `avg_*`
prefixed inference fields) ALSO exposes the unprefixed flat names
`att` / `se` / `conf_int` / `p_value` / `t_stat` as read-only `@property`
aliases over the canonical fields. The canonical prefixed fields remain
the documented and computed surface; the flat aliases are pure
read-throughs for compatibility with external adapters that
`getattr(res, "se", None)`-style query the inference surface (e.g.
`balance.interop.diff_diff.as_balance_diagnostic()`). Tables below list
the canonical names; assume the flat aliases are present on every
staggered class unless explicitly noted otherwise.

### DiDResults

Returned by `DifferenceInDifferences.fit()` and `TwoWayFixedEffects.fit()`.
Expand Down
10 changes: 10 additions & 0 deletions diff_diff/guides/llms-practitioner.txt
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,16 @@ results.cohort_effects # Per-cohort effects (via to_dataframe(level='coho
# Other staggered (ImputationDiD, TwoStageDiD, etc.):
results.overall_att # Overall ATT
results.overall_se # Standard error

# Flat-alias compatibility: every staggered class above (canonical
# `overall_*` / `overall_att_*` / `avg_*` prefixed) also exposes the
# unprefixed flat names `att` / `se` / `conf_int` / `p_value` /
# `t_stat` as read-only `@property` aliases over the canonical fields.
# The canonical prefixed names remain the documented and computed
# surface; the flat aliases are pure read-throughs for compatibility
# with adapters that `getattr(res, "se", None)`-style query inference.
results.att # alias of overall_att / avg_att (or overall_att for ContinuousDiD ATT side)
results.se # alias of overall_se / avg_se (or overall_att_se for ContinuousDiD ATT side)
```

---
Expand Down
3 changes: 2 additions & 1 deletion diff_diff/practitioner.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,8 @@ def _covariates_step() -> Dict[str, Any]:
"# Re-estimate without covariates and compare:\n"
"result_no_cov = estimator.fit(data, ..., covariates=None)\n"
"# Compare ATT with and without covariates.\n"
"# Use .att (basic DiD) or .overall_att (staggered estimators)."
"# Use .att (basic DiD; also a read-only flat-alias on staggered\n"
"# classes) or .overall_att (canonical name on staggered results)."
),
priority="medium",
step_name="robustness",
Expand Down
2 changes: 1 addition & 1 deletion docs/methodology/REGISTRY.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

This document provides the academic foundations and key implementation requirements for each estimator in diff-diff. It serves as a reference for contributors and users who want to understand the theoretical basis of the methods.

**Result-class field naming.** Headline scalar inference fields appear under one of four native naming patterns: flat `att` / `se` / `conf_int` / `p_value` / `t_stat` (`DiDResults`, `SyntheticDiDResults`, `TROPResults`, `TripleDifferenceResults`, `HeterogeneousAdoptionDiDResults`); `overall_*` (`CallawaySantAnnaResults` and the rest of the staggered family); `overall_att_*` (`ContinuousDiDResults`, where `att` and `acrt` are parallel response curves); and `avg_*` (`MultiPeriodDiDResults`). Every scalar treatment-effect result class covered by this naming contract additionally exposes the flat `att` / `se` / `conf_int` / `p_value` / `t_stat` names as read-only `@property` aliases for adapter / external-consumer compatibility (see PR for v3.3.3, motivated by `balance.interop.diff_diff`); `ContinuousDiDResults` further exposes `overall_*` aliases pointing at the ATT side. The native field is canonical for documentation, semantics, and computation — aliases are pure read-throughs and inherit the `safe_inference()` joint-NaN consistency contract automatically. Because aliases are `@property` descriptors (not dataclass fields), they do NOT appear in `dataclasses.fields()` or `dataclasses.asdict()` output, and assignment to an alias raises `AttributeError`; serializers and field-walkers continue to see only the canonical field set.
**Result-class field naming.** Headline scalar inference fields appear under one of four native naming patterns: flat `att` / `se` / `conf_int` / `p_value` / `t_stat` (`DiDResults`, `SyntheticDiDResults`, `TROPResults`, `TripleDifferenceResults`, `HeterogeneousAdoptionDiDResults`); `overall_*` (`CallawaySantAnnaResults` and the rest of the staggered family); `overall_att_*` (`ContinuousDiDResults`, where `att` and `acrt` are parallel response curves); and `avg_*` (`MultiPeriodDiDResults`). Result classes in the prefixed `overall_*` / `overall_att_*` / `avg_*` families additionally expose the flat `att` / `se` / `conf_int` / `p_value` / `t_stat` names as read-only `@property` aliases over their canonical fields, for adapter / external-consumer compatibility (see PR for v3.3.3, motivated by `balance.interop.diff_diff`). The flat-native classes (`DiDResults`, `SyntheticDiDResults`, `TROPResults`, `TripleDifferenceResults`, `HeterogeneousAdoptionDiDResults`) already carry these names as native dataclass fields and are unchanged by this contract. `ContinuousDiDResults` further exposes `overall_*` aliases pointing at the ATT side (so `result.overall_se` reads `result.overall_att_se`, etc.). The native field is canonical for documentation, semantics, and computation — aliases are pure read-throughs and inherit the `safe_inference()` joint-NaN consistency contract automatically. Because aliases are `@property` descriptors (not dataclass fields), they do NOT appear in `dataclasses.fields()` or `dataclasses.asdict()` output, and assignment to an alias raises `AttributeError`; serializers and field-walkers continue to see only the canonical field set.

## Table of Contents

Expand Down
77 changes: 66 additions & 11 deletions tests/test_result_aliases.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,24 +60,26 @@ def _required_init_kwargs(cls, overrides):
Lets us build a minimal result instance for alias-mechanic tests without
having to enumerate every estimator-specific field. Sentinel values for
untouched fields are deliberately uninteresting (empty containers, zeros)
-- they are not exercised by these tests."""
-- they are not exercised by these tests.
"""
import dataclasses as _dc

kwargs = {}
for f in fields(cls):
if f.name in overrides:
continue
# Skip fields with defaults; we only need to fill required positionals.
if f.default is not f.default_factory and f.default is not getattr(
__import__("dataclasses"), "MISSING", None
):
# Field has a default value; let the dataclass apply it.
# A field is REQUIRED iff both default and default_factory are MISSING.
# When default_factory is set (e.g. list/dict factory), the dataclass
# will apply it; we must NOT pre-fill the field with a sentinel or we
# block the factory.
if f.default is not _dc.MISSING or f.default_factory is not _dc.MISSING:
continue
# Required field — supply a type-compatible sentinel.
# Order container annotations BEFORE the scalar `"float"` / `"int"`
# branches so that ``Tuple[float, float]`` is not mis-classified as
# scalar (``"float" in "Tuple[float, float]"`` is True).
ann = str(f.type)
if "float" in ann:
kwargs[f.name] = 0.0
elif "int" in ann:
kwargs[f.name] = 0
elif "Tuple" in ann or "tuple" in ann:
if "Tuple" in ann or "tuple" in ann:
kwargs[f.name] = (0.0, 0.0)
elif "List" in ann or "list" in ann:
kwargs[f.name] = []
Expand All @@ -87,12 +89,56 @@ def _required_init_kwargs(cls, overrides):
kwargs[f.name] = pd.DataFrame()
elif "ndarray" in ann or "np.ndarray" in ann:
kwargs[f.name] = np.array([])
elif "float" in ann:
kwargs[f.name] = 0.0
elif "int" in ann:
kwargs[f.name] = 0
else:
kwargs[f.name] = None
kwargs.update(overrides)
return kwargs


def test_required_init_kwargs_handles_default_factory_and_tuple_dispatch():
"""Pin the two `_required_init_kwargs()` fixes from PR #437 R2 directly.

The helper had two latent bugs masked by the specific fields exercised
by the alias tests: factory-only required fields were pre-filled (so
the factory never ran), and the type-dispatch checked ``"float"``
before ``"Tuple"`` (so ``Tuple[float, float]`` annotations matched the
scalar branch). Both fixes are exercised here against a small local
dataclass so the contract stays pinned independent of production
result-dataclass shape.
"""
from dataclasses import dataclass, field
from typing import Tuple

@dataclass
class _Probe:
# Required, must be filled with a tuple sentinel — NOT 0.0 — even
# though "float" appears in the annotation as a substring.
ci: Tuple[float, float]
# default_factory-backed: must be OMITTED from kwargs so the
# factory runs at construction time.
items: list = field(default_factory=list)
# default-valued: must also be omitted from kwargs.
x: float = 1.5

kwargs = _required_init_kwargs(_Probe, overrides={})
assert kwargs == {"ci": (0.0, 0.0)}, (
f"_required_init_kwargs() must (a) supply a tuple sentinel for "
f"Tuple[float, float] required fields (not a scalar 0.0), and "
f"(b) omit default_factory / default fields so the dataclass "
f"applies them at construction time. Got: {kwargs!r}"
)
# Round-trip: instance construction must succeed and the factory
# must have produced an empty list (not been displaced by a sentinel).
inst = _Probe(**kwargs)
assert inst.ci == (0.0, 0.0)
assert inst.items == []
assert inst.x == 1.5


def _assert_pattern_b_aliases(res, *, att, se, t_stat, p_value, conf_int):
"""Pattern B: 5 flat aliases mapping to the overall_* canonical fields."""
assert _alias_equal(res.att, att), f"att alias != overall_att ({res.att} vs {att})"
Expand Down Expand Up @@ -304,6 +350,15 @@ def test_aliases_are_read_only(cls, ovr):
for name in ("att", "se", "conf_int", "p_value", "t_stat"):
with pytest.raises(AttributeError):
setattr(res, name, object())
# ContinuousDiDResults also exposes overall_se / overall_conf_int /
# overall_p_value / overall_t_stat as read-only aliases over the
# ATT-side canonical fields (no parallel `overall_att` alias is needed
# because `overall_att_att` would be confusing; the flat `att` covers
# that one). These must also reject assignment.
if cls.__name__ == "ContinuousDiDResults":
for name in ("overall_se", "overall_conf_int", "overall_p_value", "overall_t_stat"):
with pytest.raises(AttributeError):
setattr(res, name, object())


@pytest.mark.parametrize(
Expand Down
Loading