9  Fixed Effects

9.1 Purpose

This chapter explains why structural gravity requires fixed effects and how R-style fixed-effects workflows can be translated into Python. It also introduces double demeaning as a transparent way to understand two-way fixed effects.

9.2 Required identifiers

The Post-Soviet model variables are flow, gdp_o, gdp_d, distw, comlang_off, contig, wto_joint, EU_joint, EAEU_joint, and year. Fixed-effects models also require exporter and importer identifiers. The examples below use iso3_o and iso3_d as identifier names; if the attached dataset uses different identifier labels, replace only those identifier names.

9.3 Theory

In structural gravity, bilateral trade depends not only on direct bilateral trade costs. It also depends on each country’s trade barriers with all other partners. These broader barriers are known as multilateral resistance.

Exporter and importer fixed effects help absorb country-level differences. Exporter-year and importer-year fixed effects absorb time-varying country conditions such as GDP, aggregate demand, supply capacity, and multilateral resistance.

9.4 Equation

A basic fixed-effects gravity model is:

\[ \begin{aligned} \log(flow_{ijt}) &= \beta_1 \log(distw_{ij}) + \gamma Z_{ijt} + \alpha_i + \alpha_j + \delta_t + \varepsilon_{ijt} \end{aligned} \]

where \(Z_{ijt}\) includes institutional variables and bilateral controls.

A pair fixed-effects model is:

\[ \begin{aligned} \log(flow_{ijt}) &= \gamma_1 wto\_joint_{ijt} + \gamma_2 EU\_joint_{ijt} + \gamma_3 EAEU\_joint_{ijt} \\ &\quad + \mu_{ij} + \delta_t + \varepsilon_{ijt} \end{aligned} \]

Pair fixed effects \(\mu_{ij}\) absorb time-invariant bilateral variables such as distw, comlang_off, and contig.

9.5 Why fixed effects exist

Fixed effects exist because many determinants of trade are unobserved or difficult to measure. They reduce omitted-variable bias by comparing observations within meaningful groups: exporters, importers, years, or pairs.

9.6 Advantages

  • Controls for unobserved exporter and importer differences.
  • Controls for global shocks through year effects.
  • Pair fixed effects compare a country pair with itself over time.
  • Exporter-year and importer-year effects align with structural gravity logic.

9.7 Limitations

  • Time-invariant variables disappear under pair fixed effects.
  • High-dimensional fixed effects can be computationally heavy.
  • Fixed effects change the source of identifying variation.
  • Coefficients can weaken because the comparison becomes more demanding.
NoteAbsorbed variables

If distw, comlang_off, or contig are omitted from a pair fixed-effects model, that is not a mistake. They are absorbed by pair effects because they do not vary within a country pair over time.

9.8 R implementation

R fixed-effects workflows often use fixest.

library(dplyr)
library(fixest)

df <- read.csv("data/gravity_clean.csv")

fe_df <- df %>%
  filter(flow > 0) %>%
  mutate(
    log_flow = log(flow),
    log_distw = log(distw),
    pair_id = paste(iso3_o, iso3_d, sep = "_")
  )

fe_basic_r <- feols(
  log_flow ~ log_distw + comlang_off + contig +
    wto_joint + EU_joint + EAEU_joint |
    iso3_o + iso3_d + year,
  data = fe_df,
  vcov = "hetero"
)

fe_pair_r <- feols(
  log_flow ~ wto_joint + EU_joint + EAEU_joint |
    pair_id + year,
  data = fe_df,
  vcov = "hetero"
)

Exporter-year and importer-year fixed effects can be written as interactions:

fe_structural_r <- feols(
  log_flow ~ wto_joint + EU_joint + EAEU_joint |
    pair_id + iso3_o^year + iso3_d^year,
  data = fe_df,
  vcov = "hetero"
)

9.9 Python implementation

In Python, formula-based dummy expansion is transparent but can be slow for large panels.

import numpy as np
import pandas as pd
import statsmodels.formula.api as smf
df = pd.read_csv("data/gravity_clean.csv")

fe_df = df.loc[df["flow"] > 0].copy()
fe_df["log_flow"] = np.log(fe_df["flow"])
fe_df["log_distw"] = np.log(fe_df["distw"])
fe_df["pair_id"] = fe_df["iso3_o"] + "_" + fe_df["iso3_d"]
fe_basic_formula = (
    "log_flow ~ log_distw + comlang_off + contig + "
    "wto_joint + EU_joint + EAEU_joint + "
    "C(iso3_o) + C(iso3_d) + C(year)"
)

fe_basic_py = smf.ols(fe_basic_formula, data=fe_df).fit(cov_type="HC1")
fe_pair_formula = (
    "log_flow ~ wto_joint + EU_joint + EAEU_joint + "
    "C(pair_id) + C(year)"
)

fe_pair_py = smf.ols(fe_pair_formula, data=fe_df).fit(cov_type="HC1")

Structural fixed effects:

fe_structural_formula = (
    "log_flow ~ wto_joint + EU_joint + EAEU_joint + "
    "C(pair_id) + C(iso3_o):C(year) + C(iso3_d):C(year)"
)

fe_structural_py = smf.ols(fe_structural_formula, data=fe_df).fit(cov_type="HC1")

9.10 Double demeaning

Double demeaning explains how two-way fixed effects remove group means. For a variable \(x_{ij}\):

\[ \tilde{x}_{ij} = x_{ij} - \bar{x}_{i \cdot} - \bar{x}_{\cdot j} + \bar{x} \]

For a panel with exporter and importer groups, the transformed variable removes exporter and importer averages.

9.11 R double-demeaning workflow

double_demean <- function(x, exporter, importer) {
  x - ave(x, exporter) - ave(x, importer) + mean(x, na.rm = TRUE)
}

fe_df$log_flow_dd <- double_demean(
  fe_df$log_flow,
  fe_df$iso3_o,
  fe_df$iso3_d
)

9.12 Python double-demeaning workflow

def double_demean(data, variable, exporter, importer):
    overall = data[variable].mean()
    exporter_mean = data.groupby(exporter)[variable].transform("mean")
    importer_mean = data.groupby(importer)[variable].transform("mean")
    return data[variable] - exporter_mean - importer_mean + overall

fe_df["log_flow_dd"] = double_demean(
    fe_df,
    variable="log_flow",
    exporter="iso3_o",
    importer="iso3_d",
)

9.13 Interpretation

Fixed-effects coefficients are interpreted conditional on the absorbed effects. With pair fixed effects, wto_joint, EU_joint, and EAEU_joint are identified from within-pair changes over time. The interpretation becomes narrower and usually more credible than a pooled cross-sectional comparison.

Do not compare coefficients across fixed-effects models as if the samples and identifying variation were identical. Each specification answers a different version of the research question.

9.14 Research output

Prepare a side-by-side R-to-Python fixed-effects translation note. Include the formula, absorbed variables, estimation sample, and interpretation of the institutional coefficients.