Difference-in-Differences Is a Synthetic Control With Uniform Weights

Econometric Theory
Causal Inference
Author

Jared Greathouse

Published

September 12, 2026

This is a follow-up to my last excerpt, and (like that one) a preview of my forthcoming book on synthetic control methods.

“I think we may be In a different book, on a different page You said you are different but you’re the same” — Jhené Aiko, “stranger”


After the last post, where I showed that the arithmetic mean is really the solution to an optimization problem, somebody asked me a good question: why do people keep saying difference-in-differences (DID) is just synthetic control (SCM) with uniform weights? It is a fair thing to be confused about. The two are usually taught in different classes, with different notation, and DID in particular is almost never presented as an optimization problem at all — it is presented as a table with four cells and a subtraction, justified by parallel trends.

So let me start where everyone already is, with parallel trends. Then I will translate that familiar story into the same optimization problem we solved for the mean last time — only now with the weights held fixed — and show that, told this way, the link to synthetic control is a line of algebra rather than a slogan. At the end I will run the whole thing on Proposition 99 in Python, so you can watch the weights and the intercept do their jobs.

The same story in optimization language

Now let me translate all of that into the language of the last post, where every summary statistic turned out to be an argmin. The bridge is a single identity, and we already proved it: a mean is the solution to a least-squares problem. Attaching weights changes only the bookkeeping,

\[ \mathbb{E}_\omega[X] = \operatorname*{argmin}_{c \in \mathbb{R}} \; \mathbb{E}_\omega\!\big[(X - c)^2\big], \qquad\text{and in a sample,}\qquad \sum_i \omega_i x_i = \operatorname*{argmin}_{c \in \mathbb{R}} \sum_i \omega_i (x_i - c)^2. \]

So the weighted expectation that the practitioner’s guide puts at the center of DID is an argmin, and the \(\omega\) riding on the expectation is the very same \(\omega\) that weights the squared errors. Quote-unquote translating the notation, the control-group counterfactual is nothing but the expectation-side name for a one-parameter least-squares fit:

\[ \underbrace{\mathbb{E}_\omega[\,y_t(0) \mid D = 0\,]}_{\text{expectation notation}} \;=\; \underbrace{\operatorname*{argmin}_{c \in \mathbb{R}} \sum_{j \in \mathcal{N}_0} \omega_j \, (y_{jt} - c)^2}_{\text{optimization notation}} \;\overset{\omega \,=\, 1/N_0}{=}\; \bar{y}_{0t}. \]

Read left to right it is a population expectation; read right to left it is a weighted mean, which is a weighted argmin, which — at uniform weights — is the plain control average \(\bar{y}_{0t}\). Nothing is lost in the translation. The expectation was an optimization problem wearing different clothes, and the weights that were hidden inside it are now sitting in the open as the coefficients \(\omega_j\).

Parallel trends, translated. Write the per-period gap as

\[ g_t \defeq y_{1t} - \bar{y}_{0t}. \]

DID’s intercept is what you get by fitting a single constant to the pre-treatment gaps by least squares — the exact problem from the mean post, with the “data points” now the gaps \(g_t\):

\[ \beta^\ast = \operatorname*{argmin}_{\beta \in \mathbb{R}} \sum_{t \in \mathcal{T}_1} (g_t - \beta)^2. \]

The minimizer is the mean derivation all over again. With \(u_t(\beta) = g_t - \beta\), the chain rule gives each term’s derivative as \(2 u_t(\beta) \cdot (-1) = -2(g_t - \beta)\); summing and setting to zero,

\[ -2 \sum_{t \in \mathcal{T}_1} (g_t - \beta) = 0 \;\;\Longrightarrow\;\; \sum_{t \in \mathcal{T}_1} g_t - T_0 \beta = 0 \;\;\Longrightarrow\;\; \beta^\ast = \frac{1}{T_0} \sum_{t \in \mathcal{T}_1} g_t, \]

and the second derivative \(2 T_0 > 0\) confirms it is a minimum. So \(\beta^\ast\) is just the average pre-treatment gap — the sticky “5” from the top of the post, now estimated from the data — and the counterfactual is the control mean shifted by this one number, \(\hat{y}_{1t}(0) = \bar{y}_{0t} + \beta^\ast\).

That least-squares fit is a model of the gap: it says the gap is one constant plus noise. And this is where parallel trends drops into the same lens. In expectation notation it reads \(\mathbb{E}[g_t] = \beta\) for all \(t\); translated, that is the statement that the intercept is stable across the treatment date — the constant that best fits the pre-period gap is the same constant that best fits the post-period gap:

\[ \underbrace{\mathbb{E}[g_t] = \beta \;\; \forall \, t}_{\text{expectation notation}} \qquad\Longrightarrow\qquad \underbrace{\operatorname*{argmin}_{\beta} \, \mathbb{E}\!\!\sum_{t \in \mathcal{T}_1} (g_t - \beta)^2 \;=\; \operatorname*{argmin}_{\beta} \, \mathbb{E}\!\!\sum_{t \in \mathcal{T}_2} (g_t - \beta)^2}_{\text{optimization notation}} . \]

We only ever get to solve the left-hand problem — the post-period gaps \(g_t\) for \(t \in \mathcal{T}_2\) are unobserved, since they contain the counterfactual we are after. Parallel trends is precisely the license to carry the pre-period solution across the treatment date and use it on the right. This is the “5 never moves” claim written as an optimization: the single constant that minimizes the pre-period squared gap is the same constant that minimizes it afterward. Equivalently, the residual \(r_t = g_t - \beta^\ast\) is mean-zero with no time structure — the gap is a flat line at \(\beta^\ast\) in expectation, that same sticky number, pre and post alike.

Where synthetic control comes in

Step back and write what both DID and SCM are doing in a single line. Each builds the treated unit’s counterfactual from a weighted average of the controls, plus a level, chosen to fit the pre-treatment window:

\[ \min_{\mathbf{w}, \, \beta} \sum_{t \in \mathcal{T}_1} \Big( y_{1t} - \beta - \sum_{j \in \mathcal{N}_0} w_j \, y_{jt} \Big)^2 \qquad \text{subject to} \qquad w_j \ge 0, \quad \sum_{j \in \mathcal{N}_0} w_j = 1. \]

The constraint set — non-negative weights that sum to one — is the same simplex the arithmetic mean lives on. Recall from the last post that the mean’s weights \(w_j = 1/n\) are non-negative and sum to one, which is exactly the classic synthetic-control constraint. Synthetic control searches over \(\mathbf{w}\) on that simplex, letting the data decide which donors count. Difference-in-differences refuses to search: it plants \(\mathbf{w}\) at the dead center of the simplex, \(w_j = 1/N_0\) for every donor — the centroid — and solves only for \(\beta\). With the weights pinned, \(\sum_j w_j y_{jt}\) collapses to \(\bar{y}_{0t}\) and we are back to the one-parameter problem we just solved. That is the entire difference between the two methods: whether \(\mathbf{w}\) is free or frozen.

And now parallel trends and the weights are visibly the same conversation. The gap \(g_t = y_{1t} - \sum_j w_j y_{jt}\) carries the weights inside it, so whether \(\mathbb{E}[g_t]\) is flat is a fact about \(\mathbf{w}\), not about “California and the controls” in the abstract. Change \(\mathbf{w}\) and you change the gap, and you change whether its expectation is a constant. The two methods make opposite bets with that lever:

  • DID fixes \(\mathbf{w} = 1/N_0\) and then assumes the resulting gap is flat in expectation. Parallel trends is a hope about a gap you never got to shape.
  • SCM chooses \(\mathbf{w}\) on the simplex to make the pre-period gap as flat as it can. It buys parallel pre-trends in-sample, by construction, instead of assuming it.

So the weights were never a side issue that large-\(N\) was right to hide. They are the one lever that decides whether parallel trends is even plausible, and DID’s choice to leave that lever parked at the center of the simplex is its identifying content.

Proposition 99, with uniform weights and an intercept

Let me show the weights and the intercept in the flesh. I will use Abadie, Diamond and Hainmueller’s canonical Proposition 99 setup: California is treated by the 1989 anti-tobacco law, there are 38 control states, and we observe per-capita cigarette sales from 1970 to 2000. To follow along you’ll want the dataprep helper from my mlsynth package:

pip install -U git+https://github.com/jgreathouse9/mlsynth.git

We load the panel and hand it to dataprep, which returns the treated vector y, the donor_matrix of the 38 controls, and the number of pre-treatment periods.

import numpy as np
import pandas as pd
from mlsynth.utils.datautils import dataprep

base_url = "https://raw.githubusercontent.com/jgreathouse9/mlsynth/refs/heads/main/"
url = base_url + "basedata/smoking_data.csv"
data = pd.read_csv(url)

prepped = dataprep(
    data,
    data.columns[0],   # unit   : state
    data.columns[1],   # time   : year
    data.columns[2],   # outcome: cigsale
    data.columns[-1],  # treat  : Proposition 99
)

Now the estimator, exactly as the algebra prescribes. The uniform weights \(1/N_0\) are applied by averaging the donor matrix across its columns; the intercept \(\beta^\ast\) is the mean pre-treatment gap; the counterfactual is the shifted donor mean; and the ATT is the average post-treatment gap between California and that counterfactual.

y      = prepped["y"]
T0     = prepped["pre_periods"]
donors = prepped["donor_matrix"]
N0     = donors.shape[1]

# Uniform weights: every donor gets 1/N0. These are the SCM simplex weights,
# planted at the centroid.
w = np.full(N0, 1.0 / N0)
donor_mean = donors @ w                       # equivalently donors.mean(axis=1)

# The one free parameter: the intercept beta*.
beta = np.mean(y[:T0] - donor_mean[:T0])

yhat = donor_mean + beta                       # the DID counterfactual
ATT  = np.mean(y[T0:] - yhat[T0:])

print(f"weights sum to      {w.sum():.4f}, min weight {w.min():.4f}  (on the simplex)")
print(f"intercept  beta*  =  {beta:.4f}")
print(f"ATT              =  {ATT:.4f}")
weights sum to      1.0000, min weight 0.0263  (on the simplex)
intercept  beta*  =  -14.3590
ATT              =  -27.3491

The weights sum to one and are all positive, the intercept comes out to \(\beta^\ast \approx -14.36\) — California smoked about 14 fewer packs per capita than the average control state before the law — and the ATT is about \(-27.35\) packs per capita. Here is the picture.

Figure 2: Difference-in-Differences on Proposition 99: uniform donor weights plus an intercept.

The counterfactual does not hug California especially well before 1989 — it undershoots for the first few years and overshoots later. That misfit is the whole reason SCM exists, and it is parallel trends failing in front of you. The uniform weights force the counterfactual to be the average control state, shifted to California’s level; the intercept can only slide that line up and down, it cannot bend it. If California’s pre-period gap is not flat, no choice of \(\beta\) will make it flat, and the gap you see before 1989 is a preview of the bias you cannot see after it.

So what?

Now, you may be saying, “Hey Jared, this is a lot of algebra to conclude that a mean is a mean.” But the payoff is a clean way to see what you are actually assuming when you reach for DID, and why the choice between DID and SCM is a choice about weights and nothing else.

Both estimators answer the same question — what would California have done without Prop 99? — by averaging control states. DID commits to equal weights before it looks at the data; SCM lets the pre-treatment fit choose the weights. Neither fabricates a “fake” California or grafts together a “Frankenstein” unit, any more than the national average income fabricates a fake person. They both summarize real control units with weights that live on the same simplex. The difference is only whether those weights are handed to you as an axiom or estimated as a decision.

And the reason that decision matters shows up the moment you write untreated outcomes as a factor model, \(y_{jt}(0) = a_j + \mathbf{b}_j^\top \mathbf{f}_t + u_{jt}\). Averaging the donor pool averages its factor loadings \(\bar{\mathbf{b}}_0\), and — as I show in the book — the DID counterfactual is unbiased exactly when the treated unit’s loading equals that average, \(\mathbf{b}_1 = \bar{\mathbf{b}}_0\). That is parallel trends restated one more time: the leftover gap is \((\mathbf{b}_1 - \bar{\mathbf{b}}_0)^\top (\mathbf{f}_t - \bar{\mathbf{f}})\), a term that moves with \(t\) whenever the treated loading differs from the donor average — a gap no intercept can flatten. Uniform weights only reach California’s factor exposure if California happens to sit at the donor pool’s center of gravity. When it does not — and a state with California’s tobacco culture usually does not — you want the freedom to move the weights off the centroid, toward the donors that actually share its exposure. That freedom is synthetic control, and DID is the center of it you get by refusing to use it.

So the answer to the question I was asked is: DID is a synthetic control whose weight vector you filled in yourself, with \(1/N_0\) in every slot, before the data had a chance to weigh in. Everything else — the loss, the simplex, the intercept, the counterfactual, and parallel trends itself — is shared. You are, as ever, never not averaging. The only question is who gets to pick the weights.

What Is An Average?

Econometric Theory
Statistics

What is a Synthetic Control?

Econometrics
Causal Inference

Synthetic Controls With More Than One Outcome

Causal Inference
Econometrics

Forward Selected Synthetic Control

Machine Learning
Econometrics
No matching items