Tutorial 5: Difference-in-Differences¶
Difference-in-differences (DiD) is a popular design for causal inference. In a 2×2 DiD, we compare the change in outcomes for a treated group to the change for a control group. This tutorial shows how to compute DiD on nonlinear (logit) models using smmargins.
What you will learn¶
How to set up a DiD design with a logit outcome model
How to compute cell predictions, simple effects, and the DiD contrast
How to compute profile-specific DiD estimates
How to interpret the results
The DiD design¶
In our example, we have:
group: treatment group ("A"= control,"B"= treated)preexist_Y: pre-existing condition indicator (0 = without condition, 1 = with condition)condition_X: binary outcome (presence of a health condition)age: age in yearsfemale: gender indicator
The pre-existing condition (preexist_Y) serves as our “time” dimension: individuals with the condition represent the “post” period, and those without represent the “pre” period. The interaction between group and preexist_Y captures the DiD effect.
Step 1: Set up the data and model¶
import numpy as np
import pandas as pd
import statsmodels.formula.api as smf
from smmargins import Margins
rng = np.random.default_rng(42)
N = 6_000
df_did = pd.DataFrame({
"group": rng.choice(["A", "B"], N, p=[0.55, 0.45]),
"preexist_Y": rng.integers(0, 2, N),
"age": rng.normal(55, 15, N).clip(18, 95),
"female": rng.integers(0, 2, N),
})
eta = (-3.5 + 0.04*df_did["age"] - 0.3*df_did["female"]
+ 0.5*(df_did["group"]=="B") + 1.1*df_did["preexist_Y"]
+ 0.8*(df_did["group"]=="B")*df_did["preexist_Y"])
df_did["condition_X"] = (rng.uniform(0,1,N) < 1/(1+np.exp(-eta))).astype(int)
fit_did = smf.logit("condition_X ~ C(group) + preexist_Y + C(group):preexist_Y + age + female",
data=df_did).fit(disp=False)
M_did = Margins(fit_did)
The model includes an interaction between group and pre-existing condition, which captures the DiD effect. The coefficient on C(group)[T.B]:preexist_Y is the logit-scale interaction.
Step 2: Compute the DiD¶
Call the did() method with the group variable, the condition (“time”) variable, and their levels:
did = M_did.did("group", "preexist_Y",
group_levels=["A", "B"],
condition_levels=[0, 1])
The did object contains three components:
did.cells: Predictions in each of the four cellsdid.simple_effects: First differences (simple effects)did.did: The difference-in-differences contrast
Step 3: View the four cells¶
did.cells.summary()
| prediction | std err | z | P>|z| | [95% Conf. | Interval] | |
|---|---|---|---|---|---|---|
| group=A, preexist_Y=0 | 0.198022 | 0.009412 | 21.038469 | 2.916422e-98 | 0.179574 | 0.216470 |
| group=A, preexist_Y=1 | 0.410620 | 0.011475 | 35.784808 | 1.903121e-280 | 0.388130 | 0.433110 |
| group=B, preexist_Y=0 | 0.299717 | 0.011959 | 25.062174 | 1.286254e-138 | 0.276278 | 0.323156 |
| group=B, preexist_Y=1 | 0.716206 | 0.011964 | 59.865312 | 0.000000e+00 | 0.692758 | 0.739654 |
These are the predicted probabilities of condition_X in each of the four group-condition combinations. Notice that the outcome is higher for those with preexist_Y = 1 in both groups, and higher in group B overall.
Step 4: View the simple effects¶
The simple effects show the change from preexist_Y = 0 to preexist_Y = 1 within each group:
did.simple_effects.summary()
| simple effect | std err | z | P>|z| | [95% Conf. | Interval] | |
|---|---|---|---|---|---|---|
| group: B vs A | preexist_Y=0 | 0.101695 | 0.015219 | 6.682253 | 2.352960e-11 | 0.071867 | 0.131523 |
| group: B vs A | preexist_Y=1 | 0.305586 | 0.016580 | 18.431265 | 7.373879e-76 | 0.273090 | 0.338082 |
In group A (control), the predicted probability increases by 25.5 percentage points. In group B (treated), it increases by 33.9 percentage points.
Step 5: View the DiD contrast¶
The DiD contrast subtracts the simple effect in group A from the simple effect in group B:
did.did.summary()
| DiD | std err | z | P>|z| | [95% Conf. | Interval] | |
|---|---|---|---|---|---|---|
| DiD: group(B-A) × preexist_Y(1-0) | 0.203891 | 0.022504 | 9.06029 | 1.301042e-19 | 0.159784 | 0.247998 |
The DiD estimate is 0.0835. This means that the increase in the predicted probability of condition_X associated with having preexist_Y = 1 is 8.35 percentage points larger in group B than in group A. This is the causal effect of interest under the parallel trends assumption.
Step 6: Profile-specific DiD¶
The DiD estimate can vary by covariate profile. We can compute the DiD at specific values of age and gender using atexog:
did_profile = M_did.did("group", "preexist_Y",
group_levels=["A", "B"],
condition_levels=[0, 1],
atexog={"age": 60, "female": 0})
did_profile.cells.summary()
| prediction | std err | z | P>|z| | [95% Conf. | Interval] | |
|---|---|---|---|---|---|---|
| group=A, preexist_Y=0 | 0.241826 | 0.012700 | 19.042046 | 7.648041e-81 | 0.216935 | 0.266717 |
| group=A, preexist_Y=1 | 0.496332 | 0.015304 | 32.430587 | 1.017384e-230 | 0.466336 | 0.526328 |
| group=B, preexist_Y=0 | 0.366280 | 0.015858 | 23.097864 | 4.864630e-118 | 0.335199 | 0.397360 |
| group=B, preexist_Y=1 | 0.801633 | 0.011731 | 68.332726 | 0.000000e+00 | 0.778640 | 0.824626 |
did_profile.did.summary()
| DiD | std err | z | P>|z| | [95% Conf. | Interval] | |
|---|---|---|---|---|---|---|
| DiD: group(B-A) × preexist_Y(1-0) | 0.180847 | 0.02517 | 7.185068 | 6.717382e-13 | 0.131515 | 0.230179 |
For a 60-year-old male, the DiD estimate is 0.0938, slightly larger than the population-averaged estimate of 0.0835. This illustrates how the treatment effect can vary across covariate profiles.
Summary: the DiD workflow¶
Fit a model with the group-condition interaction
Call
M.did()with group and condition variablesInspect
cells,simple_effects, anddidtablesUse
atexogfor profile-specific estimates
Recap¶
In this tutorial we:
Set up a 2×2 DiD design with a logit outcome model
Computed predictions in the four cells
Computed simple effects (first differences) within each group
Computed the DiD contrast (difference of differences)
Showed how to get profile-specific DiD estimates with
atexog
Next steps¶
Learn about counterfactual predictions and plotting in Tutorial 6: Counterfactuals and Plotting
Read the reference for the did() method and DiD result object
Understand the Ai-Norton approach to DiD on nonlinear models in Explanation: Ai-Norton DiD
Learn about joint tests and pairwise comparisons in How-To: Joint Tests and Pairwise Comparisons