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
25 changes: 16 additions & 9 deletions causalpy/reporting.py
Original file line number Diff line number Diff line change
Expand Up @@ -1600,18 +1600,25 @@ def _compute_statistics_did_ols(
# with the degrees of freedom used below for the t-distribution.
mse = np.sum(residuals**2) / df

# Find the interaction term coefficient index
interaction_term = (
f"{result.group_variable_name}:{result.post_treatment_variable_name}"
# Find the interaction term coefficient index. patsy names interaction
# columns by formula order (e.g. "post_treatment[T.True]:group" for a
# formula written as "post_treatment*group"), so match structurally via
# the same helper algorithm() uses to locate the causal_impact
# coefficient, rather than a concatenated "group:post_treatment" string.
coeff_idx = next(
(
i
for i, label in enumerate(result.labels)
if result._is_treatment_interaction(label)
),
None,
)
coeff_idx = None
for i, label in enumerate(result.labels):
if interaction_term in label:
coeff_idx = i
break

if coeff_idx is None:
raise ValueError(f"Could not find interaction term {interaction_term} in model")
raise ValueError(
f"Could not find interaction term between '{result.group_variable_name}' "
f"and '{result.post_treatment_variable_name}' in model"
)

X = X_da
try:
Expand Down
84 changes: 84 additions & 0 deletions causalpy/tests/test_reporting.py
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,90 @@ def test_effect_summary_ols_did_residuals_are_per_observation(did_data):
assert reported_se != pytest.approx(biased_se, rel=1e-3)


@pytest.mark.integration
def test_effect_summary_ols_did_order_independent(did_data):
"""``effect_summary()`` must not depend on which variable is written
first in the DiD interaction term.

``_compute_statistics_did_ols`` looked up the interaction coefficient via
a single concatenated substring (``"group:post_treatment"``), which only
matches patsy's column naming when the formula writes the group variable
first. Writing the formula the other way round (``post_treatment*group``)
fits an identical model (``causal_impact`` was already order-independent,
fixed by #994 in ``DifferenceInDifferences.algorithm()``) but this
separate, still order-dependent lookup raised
``ValueError: Could not find interaction term ...`` instead of returning
a result.
"""
from sklearn.linear_model import LinearRegression

df = did_data

result_group_first = cp.DifferenceInDifferences(
df.copy(),
formula="y ~ 1 + group * post_treatment",
time_variable_name="t",
group_variable_name="group",
model=LinearRegression(),
)
result_post_first = cp.DifferenceInDifferences(
df.copy(),
formula="y ~ 1 + post_treatment * group",
time_variable_name="t",
group_variable_name="group",
model=LinearRegression(),
)

stats_group_first = result_group_first.effect_summary().table.loc[
"treatment_effect"
]
stats_post_first = result_post_first.effect_summary().table.loc["treatment_effect"]

for col in ("mean", "ci_lower", "ci_upper", "p_value"):
assert stats_group_first[col] == pytest.approx(stats_post_first[col])


@pytest.mark.integration
def test_effect_summary_ols_did_categorical_group_spelling(did_data):
"""``effect_summary()`` must accept the ``C(group)`` categorical spelling
of the DiD interaction.

patsy names the interaction column
``"C(group)[T.1]:post_treatment[T.True]"`` for a formula written as
``C(group) * post_treatment``; the concatenated substring lookup
(``"group:post_treatment"``) failed on it just as it did for reversed
formula order. The design matrix is numerically identical to the plain
``group * post_treatment`` spelling (``group`` is already dummy coded),
so the reported statistics must match too.
"""
from sklearn.linear_model import LinearRegression

df = did_data

result_plain = cp.DifferenceInDifferences(
df.copy(),
formula="y ~ 1 + group * post_treatment",
time_variable_name="t",
group_variable_name="group",
model=LinearRegression(),
)
result_categorical = cp.DifferenceInDifferences(
df.copy(),
formula="y ~ 1 + C(group) * post_treatment",
time_variable_name="t",
group_variable_name="group",
model=LinearRegression(),
)

stats_plain = result_plain.effect_summary().table.loc["treatment_effect"]
stats_categorical = result_categorical.effect_summary().table.loc[
"treatment_effect"
]

for col in ("mean", "ci_lower", "ci_upper", "p_value"):
assert stats_plain[col] == pytest.approx(stats_categorical[col])


@pytest.mark.integration
def test_effect_summary_ols_sc(mock_pymc_sample, sc_data):
"""Test effect_summary with OLS model for Synthetic Control."""
Expand Down