Matemáticas y Estadística 11 min de lectura 01 Mar 2026

Diferencias en Diferencias (DiD): Guía Completa con Ejemplo en Python

¿Cómo medimos el impacto real de una política pública, un tratamiento médico o una estrategia de marketing? No basta con comparar el "antes" y el "después" — necesitamos aislar el efecto causal de otros factores que cambian al mismo tiempo. Aquí es donde entra Diferencias en Diferencias (DiD), uno de los métodos más utilizados en econometría y ciencias sociales para estimar efectos causales con datos observacionales. En este artículo explicamos la teoría, la matemática formal, y cómo implementarlo paso a paso en Python.

¿Qué es Diferencias en Diferencias?

El método de Diferencias en Diferencias (en inglés, Difference-in-Differences o DiD) es una técnica de inferencia causal que estima el efecto de un tratamiento o intervención comparando los cambios a lo largo del tiempo entre un grupo de tratamiento (que recibió la intervención) y un grupo de control (que no la recibió).

La idea fundamental es simple pero elegante:

  1. Medir la diferencia en el resultado del grupo de tratamiento antes y después de la intervención
  2. Medir la diferencia en el resultado del grupo de control antes y después
  3. Calcular la diferencia de estas diferencias — eso es el efecto causal estimado
\[ \hat{\delta}_{DiD} = \underbrace{(\bar{Y}_{tratamiento}^{post} - \bar{Y}_{tratamiento}^{pre})}_{\text{Cambio en tratamiento}} - \underbrace{(\bar{Y}_{control}^{post} - \bar{Y}_{control}^{pre})}_{\text{Cambio en control}} \]

Al restar el cambio del grupo de control, eliminamos factores que afectan a ambos grupos por igual (tendencias macroeconómicas, estacionalidad, cambios tecnológicos, etc.), aislando el efecto neto de la intervención.

Intuición Visual

Imagina que un gobierno implementa un programa de capacitación laboral en la Ciudad A pero no en la Ciudad B. Queremos saber si el programa aumentó los salarios:

Antes del programa Después del programa Diferencia
Ciudad A (Tratamiento) $8,000 $10,500 +$2,500
Ciudad B (Control) $7,500 $8,500 +$1,000
DiD $2,500 - $1,000 = $1,500

Los salarios en la Ciudad A subieron $2,500, pero los de la Ciudad B (sin programa) también subieron $1,000 por factores generales (inflación, crecimiento económico). El efecto causal estimado del programa es \( \$2,500 - \$1,000 = \$1,500 \).

Tip: Sin el grupo de control, concluiríamos que el programa aumentó los salarios en $2,500 — sobreestimando el efecto real en un 67%. El grupo de control nos permite aislar cuánto hubiera cambiado el salario de todas formas.

Formulación Matemática

El modelo estándar de DiD se estima con una regresión lineal:

\[ Y_{it} = \beta_0 + \beta_1 \cdot Tratamiento_i + \beta_2 \cdot Post_t + \beta_3 \cdot (Tratamiento_i \times Post_t) + \epsilon_{it} \]

Donde:

  • \( Y_{it} \) es el resultado de la unidad \( i \) en el periodo \( t \)
  • \( Tratamiento_i \) es una variable dummy: 1 si la unidad pertenece al grupo de tratamiento, 0 si es control
  • \( Post_t \) es una variable dummy: 1 si es el periodo posterior a la intervención, 0 si es anterior
  • \( \beta_3 \) es el estimador DiD — el coeficiente de interacción que captura el efecto causal

Los coeficientes se interpretan así:

Coeficiente Interpretación
\( \beta_0 \) Media del grupo control en el periodo pre
\( \beta_1 \) Diferencia inicial entre tratamiento y control (antes de la intervención)
\( \beta_2 \) Cambio temporal que afecta a ambos grupos (tendencia común)
\( \beta_3 \) Efecto causal de la intervención (el DiD)

Desarrollando la fórmula para las cuatro combinaciones:

\[ E[Y | T=0, P=0] = \beta_0 \] \[ E[Y | T=1, P=0] = \beta_0 + \beta_1 \] \[ E[Y | T=0, P=1] = \beta_0 + \beta_2 \] \[ E[Y | T=1, P=1] = \beta_0 + \beta_1 + \beta_2 + \beta_3 \]

Entonces:

\[ \beta_3 = \underbrace{(E[Y|T=1,P=1] - E[Y|T=1,P=0])}_{\text{Cambio tratamiento}} - \underbrace{(E[Y|T=0,P=1] - E[Y|T=0,P=0])}_{\text{Cambio control}} \]

El Supuesto Clave: Tendencias Paralelas

El supuesto fundamental del DiD es el de tendencias paralelas (parallel trends assumption): en ausencia de la intervención, ambos grupos habrían seguido la misma tendencia a lo largo del tiempo.

Formalmente:

\[ E[Y_{0t} | T=1] - E[Y_{0t} | T=0] = \text{constante} \quad \forall \, t \]

Donde \( Y_{0t} \) es el resultado potencial sin tratamiento. Esto significa que la brecha entre ambos grupos se mantendría constante si no hubiera intervención. No se requiere que los grupos sean idénticos — solo que evolucionen de forma paralela.

Este supuesto no se puede verificar directamente (porque no observamos qué hubiera pasado sin tratamiento), pero se puede evaluar indirectamente:

  • Verificar que las tendencias sean paralelas en periodos previos a la intervención
  • Realizar placebo tests: aplicar DiD en periodos donde no hubo intervención
  • Incluir covariables que puedan explicar diferencias en tendencias

Implementación en Python: Ejemplo Completo

Vamos a implementar un análisis DiD completo con datos simulados. El escenario: una empresa implementa un programa de bienestar laboral en sus oficinas de Ciudad de México, pero no en Guadalajara. Queremos estimar el efecto del programa sobre la productividad de los empleados.

Paso 1: Generar los datos

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import statsmodels.formula.api as smf

np.random.seed(42)

n_empleados = 500  # por grupo

# Grupo de tratamiento (CDMX): recibe programa en t=1
productividad_tratamiento_pre = np.random.normal(75, 10, n_empleados)
productividad_tratamiento_post = productividad_tratamiento_pre + np.random.normal(8, 5, n_empleados)  # +8 efecto real del programa + tendencia

# Grupo de control (GDL): no recibe programa
productividad_control_pre = np.random.normal(72, 10, n_empleados)
productividad_control_post = productividad_control_pre + np.random.normal(3, 5, n_empleados)  # +3 solo tendencia natural

# Crear DataFrame
df = pd.DataFrame({
    'empleado_id': list(range(1, n_empleados*2 + 1)) * 2,
    'productividad': np.concatenate([
        productividad_tratamiento_pre, productividad_control_pre,
        productividad_tratamiento_post, productividad_control_post
    ]),
    'tratamiento': np.concatenate([
        np.ones(n_empleados), np.zeros(n_empleados),
        np.ones(n_empleados), np.zeros(n_empleados)
    ]).astype(int),
    'post': np.concatenate([
        np.zeros(n_empleados*2), np.ones(n_empleados*2)
    ]).astype(int),
    'periodo': ['Pre-programa'] * (n_empleados*2) + ['Post-programa'] * (n_empleados*2),
    'grupo': (['CDMX (Tratamiento)'] * n_empleados + ['GDL (Control)'] * n_empleados) * 2
})

print(df.groupby(['grupo', 'periodo'])['productividad'].agg(['mean', 'std', 'count']))
print(f"\nTotal observaciones: {len(df)}")

Paso 2: Visualizar tendencias paralelas

import matplotlib.pyplot as plt
import numpy as np

# Calcular medias por grupo y periodo
medias = df.groupby(['periodo', 'grupo'])['productividad'].mean().unstack()

# Reordenar periodos
medias = medias.reindex(['Pre-programa', 'Post-programa'])

fig, ax = plt.subplots(figsize=(10, 6))

# Líneas observadas
ax.plot(['Pre', 'Post'], medias['CDMX (Tratamiento)'], 'o-', color='#6366f1',
        linewidth=2.5, markersize=10, label='CDMX (Tratamiento)')
ax.plot(['Pre', 'Post'], medias['GDL (Control)'], 's-', color='#f97316',
        linewidth=2.5, markersize=10, label='GDL (Control)')

# Línea contrafactual (tendencia paralela)
contrafactual = medias['CDMX (Tratamiento)'].iloc[0] + (
    medias['GDL (Control)'].iloc[1] - medias['GDL (Control)'].iloc[0]
)
ax.plot(['Pre', 'Post'], [medias['CDMX (Tratamiento)'].iloc[0], contrafactual],
        'o--', color='#6366f1', alpha=0.4, linewidth=2, markersize=8,
        label='Contrafactual (sin programa)')

# Marcar el efecto DiD
efecto = medias['CDMX (Tratamiento)'].iloc[1] - contrafactual
ax.annotate(f'Efecto DiD\n≈ {efecto:.1f} puntos',
            xy=('Post', contrafactual + efecto/2),
            xytext=(1.15, contrafactual + efecto/2),
            fontsize=12, fontweight='bold', color='#6366f1',
            arrowprops=dict(arrowstyle='->', color='#6366f1'))

ax.set_xlabel('Periodo', fontsize=12)
ax.set_ylabel('Productividad promedio', fontsize=12)
ax.set_title('Diferencias en Diferencias: Efecto del Programa de Bienestar', fontsize=14)
ax.legend(fontsize=11)
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('did_visualization.png', dpi=150)
plt.show()

En la gráfica, la línea punteada muestra el contrafactual: cómo habría evolucionado el grupo de tratamiento sin el programa (asumiendo tendencias paralelas). La distancia vertical entre la línea observada y la punteada es el efecto DiD.

Paso 3: Estimación con regresión (OLS)

import statsmodels.formula.api as smf

# Modelo DiD estándar
modelo_did = smf.ols('productividad ~ tratamiento + post + tratamiento:post', data=df).fit()

print("=" * 60)
print("RESULTADOS DEL MODELO DiD")
print("=" * 60)
print(modelo_did.summary().tables[1])

print(f"""
Interpretación de coeficientes:
─────────────────────────────────────────────
β0 (Intercept):      {modelo_did.params['Intercept']:.2f}  → Media control pre-programa
β1 (tratamiento):    {modelo_did.params['tratamiento']:.2f}  → Diferencia inicial CDMX vs GDL
β2 (post):           {modelo_did.params['post']:.2f}  → Tendencia natural (ambos grupos)
β3 (tratamiento:post): {modelo_did.params['tratamiento:post']:.2f}  → ★ EFECTO CAUSAL DiD ★
─────────────────────────────────────────────
p-valor del efecto:  {modelo_did.pvalues['tratamiento:post']:.6f}
IC 95%: [{modelo_did.conf_int().loc['tratamiento:post'][0]:.2f}, {modelo_did.conf_int().loc['tratamiento:post'][1]:.2f}]
R²:                  {modelo_did.rsquared:.4f}
""")

# Verificación manual
media_t_pre = df[(df.tratamiento==1) & (df.post==0)]['productividad'].mean()
media_t_post = df[(df.tratamiento==1) & (df.post==1)]['productividad'].mean()
media_c_pre = df[(df.tratamiento==0) & (df.post==0)]['productividad'].mean()
media_c_post = df[(df.tratamiento==0) & (df.post==1)]['productividad'].mean()

did_manual = (media_t_post - media_t_pre) - (media_c_post - media_c_pre)
print(f"DiD calculado manualmente: {did_manual:.2f}")
print(f"DiD de la regresión:       {modelo_did.params['tratamiento:post']:.2f}")
print(f"✓ Ambos coinciden")

Paso 4: DiD con covariables

En la práctica, es común agregar covariables para mejorar la precisión y controlar por factores que podrían afectar el resultado:

# Agregar covariables al dataset
df['experiencia'] = np.tile(
    np.concatenate([
        np.random.uniform(1, 20, n_empleados),
        np.random.uniform(1, 18, n_empleados)
    ]), 2
)
df['edad'] = np.tile(
    np.concatenate([
        np.random.normal(35, 8, n_empleados),
        np.random.normal(33, 7, n_empleados)
    ]), 2
)

# Modelo DiD con covariables
modelo_cov = smf.ols(
    'productividad ~ tratamiento + post + tratamiento:post + experiencia + edad',
    data=df
).fit()

print("Modelo DiD con covariables:")
print(modelo_cov.summary().tables[1])
print(f"\nEfecto DiD (con covariables): {modelo_cov.params['tratamiento:post']:.2f}")
print(f"Efecto DiD (sin covariables): {modelo_did.params['tratamiento:post']:.2f}")
print(f"Diferencia: {abs(modelo_cov.params['tratamiento:post'] - modelo_did.params['tratamiento:post']):.2f}")

Tip: Las covariables no cambian el estimador DiD si el supuesto de tendencias paralelas se cumple, pero pueden reducir la varianza del estimador y hacer los intervalos de confianza más estrechos. Si el efecto cambia mucho al agregar covariables, es señal de que el supuesto de tendencias paralelas podría estar violado.

Prueba de Tendencias Paralelas (Placebo Test)

La forma más común de evaluar el supuesto de tendencias paralelas es un placebo test: verificar que no hay "efecto" en periodos donde no hubo intervención. Si tenemos datos de múltiples periodos previos, podemos simular esto:

import numpy as np
import pandas as pd
import statsmodels.formula.api as smf

np.random.seed(123)
n = 300

# Simular 4 periodos: t=0,1 (pre) y t=2,3 (post)
# Intervención ocurre entre t=1 y t=2
periodos_data = []
for t in range(4):
    tendencia = t * 2  # Tendencia común: +2 por periodo
    efecto = 5 if t >= 2 else 0  # Efecto del programa solo en t=2,3

    # Tratamiento
    prod_t = np.random.normal(70 + tendencia + efecto, 8, n)
    for val in prod_t:
        periodos_data.append({
            'productividad': val, 'tratamiento': 1, 'periodo': t, 'post': int(t >= 2)
        })

    # Control
    prod_c = np.random.normal(68 + tendencia, 8, n)
    for val in prod_c:
        periodos_data.append({
            'productividad': val, 'tratamiento': 0, 'periodo': t, 'post': int(t >= 2)
        })

df_multi = pd.DataFrame(periodos_data)

# Placebo test: DiD solo con periodos PRE (t=0 vs t=1)
df_placebo = df_multi[df_multi.periodo.isin([0, 1])].copy()
df_placebo['post_placebo'] = (df_placebo.periodo == 1).astype(int)

modelo_placebo = smf.ols(
    'productividad ~ tratamiento + post_placebo + tratamiento:post_placebo',
    data=df_placebo
).fit()

# DiD real: periodos pre (0,1) vs post (2,3)
modelo_real = smf.ols(
    'productividad ~ tratamiento + post + tratamiento:post',
    data=df_multi
).fit()

print("PLACEBO TEST (periodos pre-intervención)")
print(f"  Efecto DiD placebo: {modelo_placebo.params['tratamiento:post_placebo']:.3f}")
print(f"  p-valor:            {modelo_placebo.pvalues['tratamiento:post_placebo']:.4f}")
print(f"  ¿Significativo?     {'Sí ⚠️ (tendencias NO paralelas)' if modelo_placebo.pvalues['tratamiento:post_placebo'] < 0.05 else 'No ✓ (tendencias paralelas)'}")

print(f"\nDiD REAL (pre vs post intervención)")
print(f"  Efecto DiD:         {modelo_real.params['tratamiento:post']:.3f}")
print(f"  p-valor:            {modelo_real.pvalues['tratamiento:post']:.6f}")
print(f"  ¿Significativo?     {'Sí ✓' if modelo_real.pvalues['tratamiento:post'] < 0.05 else 'No'}")

Si el placebo test muestra un efecto significativo en el periodo pre, el supuesto de tendencias paralelas está comprometido y los resultados del DiD podrían estar sesgados.

Extensiones del DiD

DiD con efectos fijos (Two-Way Fixed Effects)

Con datos de panel (múltiples unidades observadas en múltiples periodos), el modelo estándar se extiende con efectos fijos por unidad y por periodo:

\[ Y_{it} = \alpha_i + \gamma_t + \beta \cdot D_{it} + \epsilon_{it} \]

Donde \( \alpha_i \) captura diferencias fijas entre unidades (que no cambian en el tiempo) y \( \gamma_t \) captura shocks comunes a todas las unidades en cada periodo. Esta especificación es más robusta que el DiD básico porque controla por heterogeneidad no observada.

DiD escalonado (Staggered DiD)

En muchos casos reales, la intervención no se aplica a todos los tratados al mismo tiempo. Por ejemplo, diferentes estados adoptan una ley en diferentes años. El DiD escalonado maneja esta complejidad, aunque investigaciones recientes (Goodman-Bacon 2021, Callaway & Sant'Anna 2021) han demostrado que el estimador TWFE estándar puede ser sesgado en estos casos. Librerías como did en R o pydid en Python implementan estimadores robustos.

Limitaciones y Cuándo NO Usar DiD

  • Violación de tendencias paralelas: Si los grupos ya tenían tendencias divergentes antes de la intervención, el DiD estará sesgado. Siempre verifica con placebo tests.
  • Anticipación: Si el grupo de tratamiento cambia su comportamiento antes de la intervención (porque sabe que viene), el estimador estará contaminado.
  • Spillovers: Si la intervención en el grupo de tratamiento afecta indirectamente al grupo de control, el efecto estará subestimado.
  • Composición cambiante: Si la composición de los grupos cambia entre periodos (e.g., migración selectiva), el supuesto se viola.
  • Un solo grupo tratado: Con solo una unidad tratada (un estado, una empresa), la inferencia estadística es débil. Considera control sintético como alternativa.

Aplicaciones Reales del DiD

Estudio Intervención Hallazgo
Card & Krueger (1994) Aumento del salario mínimo en New Jersey No redujo el empleo en fast food (contraintuitivo)
Autor (2003) Ley de protección al empleo en EE.UU. Redujo flujos de trabajadores y productividad
Duflo (2001) Construcción de escuelas en Indonesia Aumentó años de escolaridad y salarios
Finkelstein (2007) Introducción de Medicare en EE.UU. Aumentó el gasto hospitalario significativamente

Conclusión

Diferencias en Diferencias es una de las herramientas más poderosas y accesibles de la econometría moderna para estimar efectos causales. Su elegancia radica en que, al comparar los cambios entre grupos en lugar de los niveles, elimina sesgos por diferencias permanentes entre los grupos.

En este artículo cubrimos:

  • La intuición detrás del método: por qué necesitamos un grupo de control y qué es el contrafactual
  • La formulación matemática completa: el modelo de regresión con término de interacción \( \beta_3 \)
  • El supuesto de tendencias paralelas: qué significa, por qué es crucial, y cómo evaluarlo
  • Implementación en Python paso a paso: generación de datos, visualización, estimación con OLS, covariables y placebo tests
  • Extensiones: efectos fijos (TWFE) y DiD escalonado
  • Limitaciones: cuándo el método no es apropiado
  • Aplicaciones reales clásicas en economía y política pública

Si te interesa profundizar en inferencia causal, te recomendamos explorar otros métodos complementarios como Regression Discontinuity Design (RDD), Variables Instrumentales (IV), y Control Sintético. Cada uno tiene sus fortalezas y supuestos, y un buen investigador sabe cuándo aplicar cada uno.