Does Temperature Shape Economic Growth? Replicating Burke, Hsiang & Miguel

A replication of a landmark climate economics paper that found an inverted-U relationship between temperature and GDP growth — and what it means for countries like Indonesia.
r
causal-inference
development-economics
climate
Author

Nadhira A. Hendra

Published

March 18, 2026

Modified

March 18, 2026

Introduction

I grew up in Indonesia, a country that sits almost entirely within the tropics, averaging annual temperatures well above 25°C. It’s hot. Always. And for years, I’d hear the usual narrative: warm countries are “naturally” less productive, cooler climates breed harder workers, and the economic gap between the Global North and South was somehow baked into geography.

This is the reason why this paper intrigues me.

Burke, Hsiang, and Miguel published “Global non-linear effect of temperature on economic production” in Nature in 2015 and offered one of the most compelling empirical answers to a deceptively simple question: does temperature actually affect how much countries grow?

In short, their answer is yes, and the paper reveals a striking non-linear relationship between temperature and economic development.

This post is a replication of their core analysis. I reproduce their main findings using the same dataset, then add a robustness check using an alternative growth specification. By the end, I want to make two things clear: first, the temperature-GDP relationship is robust and well-identified; and second, countries like Indonesia, which are already sitting in the hottest part of the curve, are facing compounding risks from further warming.

Research Design

The paper’s core argument can be summarized in three points:

  • The relationship between temperature and GDP growth is an inverted U-shape. GDP growth increases with temperature up to an optimal point, around 13°C in the original paper, then declines sharply.

  • The identification strategy relies on within-country temperature variation over time. Country and year fixed effects remove time-invariant confounders such as geography, institutions, and culture, as well as global shocks like recessions and pandemics. What remains is the within-country, year-to-year variation in temperature, which is plausibly exogenous to economic decisions.

  • The damage from future warming falls disproportionately on poor, hot countries. Rich countries cluster near the optimal temperature. Poor countries in the tropics are already on the declining side, and additional warming only makes things worse.

The core regression model is:

\[\Delta \ln(\text{GDP per capita})_{it} = \beta_1 T_{it} + \beta_2 T_{it}^2 + \gamma P_{it} + \lambda P_{it}^2 + \alpha_i + \delta_t + \varepsilon_{it}\]

Where \(T_{it}\) is the population-weighted annual average temperature, \(P_{it}\) is precipitation, \(\alpha_i\) are country fixed effects, and \(\delta_t\) are year fixed effects. The inverted-U shape is identified by a negative \(\beta_2\), and the optimal temperature is found by setting the first derivative equal to zero: \(T^* = -\beta_1 / (2\beta_2)\).

Data

The dataset merges several sources:

  • World Development Indicators (WDI): GDP per capita growth rates for 166 countries over approximately 50 years
  • University of Delaware climate data: Population-weighted country-level temperature and precipitation
  • Additional economic variables: Agricultural and non-agricultural GDP, total population, PPP-adjusted income

Population weighting is important: rather than averaging temperature uniformly across a country’s land area, the variable represents the average temperature experienced by a person in that country. Since economic activity concentrates in populated areas, this is a more relevant measure for growth regressions.

Code
raw_data <- read.csv("data/bhm_data.csv")

# Drop observations missing outcome or main regressor
bhm_data <- subset(raw_data,
                   !is.na(raw_data$growthWDI) &
                   !is.na(raw_data$UDel_temp_popweight))

cat("Countries:", n_distinct(bhm_data$iso), "\n")
Countries: 166 
Code
cat("Years:", n_distinct(bhm_data$year), "\n")
Years: 50 
Code
cat("Observations:", nrow(bhm_data), "\n")
Observations: 6584 

Descriptive Statistics

Code
selected_vars <- c("growthWDI", "UDel_temp_popweight", "UDel_precip_popweight")

desc_table <- bhm_data %>%
  summarise(across(all_of(selected_vars),
                   list(
                     Mean   = ~round(mean(., na.rm = TRUE), 2),
                     SD     = ~round(sd(., na.rm = TRUE), 2),
                     Min    = ~round(min(., na.rm = TRUE), 2),
                     Median = ~round(median(., na.rm = TRUE), 2),
                     Max    = ~round(max(., na.rm = TRUE), 2)
                   ))) %>%
  pivot_longer(everything(),
               names_to = c("Variable", ".value"),
               names_sep = "_(?=[^_]+$)") %>%
  mutate(Variable = recode(Variable,
    "growthWDI"              = "GDP per capita growth (%)",
    "UDel_temp_popweight"    = "Temperature, pop-weighted (°C)",
    "UDel_precip_popweight"  = "Precipitation, pop-weighted (mm/month)"
  ))

desc_table %>%
  kable(booktabs = TRUE, align = c("l", "c", "c", "c", "c", "c")) %>%
  kable_styling(
    bootstrap_options = c("striped", "hover", "condensed"),
    full_width = FALSE,
    position = "center"
  ) %>%
  footnote(
    general = "Final sample after dropping observations missing growth or temperature data. 166 countries, ~50 years.",
    general_title = "Note:",
    footnote_as_chunk = TRUE
  )
Table 1: Summary Statistics
Variable Mean SD Min Median Max
GDP per capita growth (%) 0.02 0.06 -0.70 0.02 0.88
Temperature, pop-weighted (°C) 18.92 7.46 -6.49 20.88 29.61
Precipitation, pop-weighted (mm/month) 1157.93 739.87 9.12 994.91 4877.74
Note: Final sample after dropping observations missing growth or temperature data. 166 countries, ~50 years.

A few observations worth flagging:

  • Growth rates range from severe contractions to rapid expansions; 50% of observations fall between roughly −0.3% and 4% annually, which is reasonable for typical country-year variation.
  • Temperature has a mean of about 19°C but high variance (SD ≈ 9°C). The interquartile range spans 12.9°C to 25.5°C, reflecting the wide diversity of climates in the sample.
  • The gap between mean (18.9°C) and median (20.9°C) for temperature suggests left-skew: there are relatively few very cold country-years pulling the mean down.

Main Regression: The Quadratic Temperature-GDP Model

Estimation

I estimate the model using feols from the fixest package, which handles high-dimensional fixed effects efficiently. The main specification includes country fixed effects, denoted \(\alpha_i\), and year fixed effects \(\delta_t\).

Code
# Create quadratic terms
bhm_data$temp_sq   <- bhm_data$UDel_temp_popweight^2
bhm_data$precip_sq <- bhm_data$UDel_precip_popweight^2

# Main model: quadratic temperature + precipitation, country + year FE
model_main <- feols(
  growthWDI ~ UDel_temp_popweight + temp_sq +
              UDel_precip_popweight + precip_sq | iso + year,
  data = bhm_data
)

# Extract key coefficients
b1 <- coef(model_main)["UDel_temp_popweight"]
b2 <- coef(model_main)["temp_sq"]

# Optimal temperature
T_star <- -b1 / (2 * b2)

cat("β₁ (temp):   ", round(b1, 5), "\n")
β₁ (temp):    0.00844 
Code
cat("β₂ (temp²):  ", round(b2, 6), "\n")
β₂ (temp²):   -0.000249 
Code
cat("Optimal T*:  ", round(T_star, 2), "°C\n")
Optimal T*:   16.95 °C

Regression Output

Code
modelsummary(
  list("GDP Growth (WDI)" = model_main),
  coef_map = c(
    "UDel_temp_popweight"   = "Temperature (°C)",
    "temp_sq"               = "Temperature² (°C²)",
    "UDel_precip_popweight" = "Precipitation",
    "precip_sq"             = "Precipitation²"
  ),
  gof_map = c("nobs", "r.squared", "adj.r.squared"),
  stars   = TRUE,
  title   = "Dependent variable: GDP per capita growth rate (%)",
  notes   = "Country and year fixed effects included. Standard errors clustered by country."
)
Table 2: Main Regression: Temperature and GDP per Capita Growth
Dependent variable: GDP per capita growth rate (%)
GDP Growth (WDI)
+ p < 0.1, * p < 0.05, ** p < 0.01, *** p < 0.001
Country and year fixed effects included. Standard errors clustered by country.
Temperature (°C) 0.008**
(0.003)
Temperature² (°C²) -0.000**
(0.000)
Precipitation 0.000+
(0.000)
Precipitation² -0.000
(0.000)
Num.Obs. 6584
R2 0.153
R2 Adj. 0.124

Both temperature coefficients are highly significant. \(\beta_1 > 0\) and \(\beta_2 < 0\) confirms the inverted-U shape: growth rises with temperature up to the optimal point, then falls. My estimated optimal temperature is around 16.9°C, somewhat higher than the paper’s reported 13°C, which the authors obtained using a slightly different specification. The qualitative pattern is the same.

Note

Why fixed effects support a causal interpretation, and where the assumptions bite.

Country fixed effects absorb time-invariant confounders: geography, institutions, culture, and historical development patterns. Year fixed effects absorb global shocks: recessions, oil prices, global technology shocks. What remains is within-country, within-year temperature variation, the year-to-year deviations from a country’s typical climate. This variation is plausibly exogenous to economic decisions.

The key assumptions are strict exogeneity, meaning temperature shocks are not caused by GDP changes, no time-varying omitted confounders within countries, and no spillovers across borders. These are defensible but not guaranteed.

Visualizing the Response Function

The clearest way to see the result is to plot predicted GDP growth against temperature, holding everything else constant. This is Figure 2 from the original paper.

Code
# Prediction grid
temp_range <- seq(-5, 35, by = 0.5)

# Predicted growth (linear + quadratic terms)
y_hat <- b1 * temp_range + b2 * temp_range^2

# Delta-method standard errors: SE(ŷ) = sqrt(X' Var(β) X)
vcov_mat <- vcov(model_main)[c("UDel_temp_popweight", "temp_sq"),
                              c("UDel_temp_popweight", "temp_sq")]

se_fit <- sapply(temp_range, function(t) {
  x <- c(t, t^2)
  sqrt(as.numeric(t(x) %*% vcov_mat %*% x))
})

# Center predictions at the maximum
y_center   <- y_hat - max(y_hat)
ci_lower   <- y_hat - 1.96 * se_fit - max(y_hat)
ci_upper   <- y_hat + 1.96 * se_fit - max(y_hat)

pred_df <- data.frame(
  temp      = temp_range,
  y         = y_center,
  ci_lower  = ci_lower,
  ci_upper  = ci_upper
)

# Histogram data (0.5°C bins)
bins <- seq(-6, 30, by = 0.5)
bin_mids <- bins[-length(bins)] + 0.25

hist_temp <- bhm_data$UDel_temp_popweight
hist_pop  <- bhm_data$Pop
hist_gdp  <- bhm_data$TotGDP

n_bins <- length(bins) - 1
pop_by_bin   <- numeric(n_bins)
gdp_by_bin   <- numeric(n_bins)
count_by_bin <- numeric(n_bins)

for (j in seq_len(n_bins)) {
  in_bin           <- hist_temp >= bins[j] & hist_temp < bins[j + 1]
  count_by_bin[j]  <- sum(in_bin)
  pop_by_bin[j]    <- sum(hist_pop[in_bin], na.rm = TRUE)
  gdp_by_bin[j]    <- sum(hist_gdp[in_bin], na.rm = TRUE)
}

# Normalize
scale_factor <- 0.06
count_norm <- count_by_bin / max(count_by_bin) * scale_factor
pop_norm   <- pop_by_bin   / max(pop_by_bin)   * scale_factor
gdp_norm   <- gdp_by_bin   / max(gdp_by_bin)   * scale_factor

base_y  <- -0.35
spacing <- 0.065

hist_df <- data.frame(
  xmin = bins[-length(bins)],
  xmax = bins[-1],
  temp_h  = base_y,
  temp_t  = base_y + count_norm,
  pop_h   = base_y - spacing,
  pop_t   = base_y - spacing + pop_norm,
  gdp_h   = base_y - 2 * spacing,
  gdp_t   = base_y - 2 * spacing + gdp_norm
)

# Country reference lines
selected <- data.frame(
  iso   = c("DEU", "USA", "NGA", "IDN", "GBR", "FRA", "CHN", "BRA"),
  label = c("Germany", "USA", "Nigeria", "Indonesia", "UK", "France", "China", "Brazil")
)

country_temps <- bhm_data %>%
  filter(iso %in% selected$iso) %>%
  group_by(iso) %>%
  summarise(mean_temp = mean(UDel_temp_popweight, na.rm = TRUE)) %>%
  left_join(selected, by = "iso")

ggplot() +
  # CI ribbon
  geom_ribbon(data = pred_df,
              aes(x = temp, ymin = ci_lower, ymax = ci_upper),
              fill = "#6BAED6", alpha = 0.4) +
  # Response curve
  geom_line(data = pred_df,
            aes(x = temp, y = y),
            color = "#08519C", linewidth = 0.9) +
  # Zero reference line
  geom_hline(yintercept = 0, linetype = "dashed", color = "gray60", linewidth = 0.4) +
  # Country vertical lines
  geom_segment(data = country_temps,
               aes(x = mean_temp, xend = mean_temp, y = -0.26, yend = 0.07),
               color = "gray60", linewidth = 0.3) +
  geom_text(data = country_temps,
            aes(x = mean_temp, y = 0.08, label = label),
            angle = 90, hjust = 0, size = 2.5, color = "gray40") +
  # Temperature histogram (red)
  geom_rect(data = hist_df,
            aes(xmin = xmin, xmax = xmax, ymin = temp_h, ymax = temp_t),
            fill = "#CB181D", color = NA, alpha = 0.8) +
  # Population histogram (gray)
  geom_rect(data = hist_df,
            aes(xmin = xmin, xmax = xmax, ymin = pop_h, ymax = pop_t),
            fill = "gray50", color = NA, alpha = 0.8) +
  # GDP histogram (black)
  geom_rect(data = hist_df,
            aes(xmin = xmin, xmax = xmax, ymin = gdp_h, ymax = gdp_t),
            fill = "gray15", color = NA, alpha = 0.8) +
  annotate("text", x = -4.5, y = base_y + 0.005,           label = "Obs.",  hjust = 0, size = 2.2, color = "#CB181D") +
  annotate("text", x = -4.5, y = base_y - spacing + 0.005,     label = "Pop.",  hjust = 0, size = 2.2, color = "gray50") +
  annotate("text", x = -4.5, y = base_y - 2*spacing + 0.005,   label = "GDP",   hjust = 0, size = 2.2, color = "gray15") +
  scale_x_continuous(limits = c(-5, 35),
                     breaks = seq(0, 30, by = 10),
                     name = "Annual average temperature (°C)") +
  scale_y_continuous(name = "Change in ln(GDP per capita)\n(relative to peak)",
                     limits = c(base_y - 2.5 * spacing, 0.18)) +
  theme_minimal(base_size = 11) +
  theme(
    panel.grid.minor = element_blank(),
    axis.title       = element_text(size = 10),
    plot.caption     = element_text(size = 8, color = "gray50")
  )
Figure 1: Estimated effect of temperature on GDP per capita growth. The curve shows predicted growth relative to the optimal temperature (centered at 0). Shaded band is a 95% confidence interval. Histograms at the bottom show the distribution of temperature-weighted observations (red), population (gray), and GDP (black) across temperature bins. Vertical lines mark mean temperatures for selected countries.

The curve tells a clear story. The optimal temperature for economic growth is around 17°C, roughly where Germany and France sit. Countries in the tropics (Nigeria, Indonesia, Brazil) are already well past the peak; every degree of additional warming pushes them further down the declining slope.

The histograms at the bottom also reveal something troubling: most of the world’s population (gray) and economic activity (black) is concentrated in warm regions already on the right side of the curve.

Robustness: Alternative Growth Specification

The original paper uses growthWDI, which is the World Bank’s pre-computed GDP per capita growth rate. A theoretically cleaner alternative is to compute growth directly as the log difference in GDP per capita:

\[g_{it}^{\text{log}} = \ln\left(\frac{\text{GDP}_{it}}{\text{Pop}_{it}}\right) - \ln\left(\frac{\text{GDP}_{i,t-1}}{\text{Pop}_{i,t-1}}\right)\]

This is equivalent to the WDI measure for small growth rates, within roughly ±10%, but avoids reliance on the WDI’s data processing choices.

Code
# Compute log-difference growth from raw GDP and population
bhm_data <- bhm_data %>%
  arrange(iso, year) %>%
  group_by(iso) %>%
  mutate(
    gdp_per_cap  = TotGDP / Pop,
    ln_gdp_pc    = log(gdp_per_cap),
    growth_logdiff = ln_gdp_pc - lag(ln_gdp_pc)
  ) %>%
  ungroup()

# Replicate model with log-difference outcome
model_logdiff <- feols(
  growth_logdiff ~ UDel_temp_popweight + temp_sq +
                   UDel_precip_popweight + precip_sq | iso + year,
  data = bhm_data
)

b1_ld <- coef(model_logdiff)["UDel_temp_popweight"]
b2_ld <- coef(model_logdiff)["temp_sq"]
T_star_ld <- -b1_ld / (2 * b2_ld)

cat("Log-diff model — Optimal T*:", round(T_star_ld, 2), "°C\n")
Log-diff model — Optimal T*: 16.47 °C

Model Comparison

Code
modelsummary(
  list(
    "WDI Growth Rate"     = model_main,
    "Log-Difference Growth" = model_logdiff
  ),
  coef_map = c(
    "UDel_temp_popweight"   = "Temperature (°C)",
    "temp_sq"               = "Temperature² (°C²)",
    "UDel_precip_popweight" = "Precipitation",
    "precip_sq"             = "Precipitation²"
  ),
  gof_map = c("nobs", "r.squared", "adj.r.squared"),
  stars   = TRUE,
  notes   = "Country and year fixed effects in all models. Standard errors clustered by country."
)
Table 3: Main Model vs. Log-Difference Robustness Check
WDI Growth Rate Log-Difference Growth
+ p < 0.1, * p < 0.05, ** p < 0.01, *** p < 0.001
Country and year fixed effects in all models. Standard errors clustered by country.
Temperature (°C) 0.008** 0.008**
(0.003) (0.003)
Temperature² (°C²) -0.000** -0.000**
(0.000) (0.000)
Precipitation 0.000+ 0.000
(0.000) (0.000)
Precipitation² -0.000 -0.000
(0.000) (0.000)
Num.Obs. 6584 6418
R2 0.153 0.155
R2 Adj. 0.124 0.125

The sign and significance of the temperature coefficients are consistent across both specifications. The optimal temperature estimate shifts slightly, a reminder that the exact turning point is sensitive to functional form and data construction, but the core inverted-U shape is robust.

What This Means

Two takeaways stand out.

First, the identification strategy is genuinely clever. By using within-country variation in temperature over time, the authors sidestep the usual concern that cold countries are richer for historical or institutional reasons unrelated to climate. They’re not comparing Norway to Nigeria; they’re asking whether Norway grows faster in unusually warm years. That’s a much cleaner question.

Second, the distributional implications are stark. Countries already sitting in warm climates, which largely overlap with countries that are poorer and less able to adapt, face the steepest portion of the decline curve. Climate change isn’t just a global average problem; it’s a deeply unequal one. Indonesia, sitting at around 26°C, would lose roughly 0.5–1 percentage point of annual growth per degree of warming according to these estimates. Compounded over decades, that’s enormous.

Of course, GDP is an imperfect welfare measure. As (Burke, Hsiang, and Miguel 2015) themselves acknowledge, it misses health impacts, mortality, inequality, and ecosystem damage that may be equally or more important. The National Academies of Sciences have flagged exactly this limitation. But as a tractable, cross-country measure available over a long horizon, it remains the best available instrument for estimating aggregate economic damages.

Conclusion

This replication confirms the main findings of Burke, Hsiang, and Miguel: temperature has a significant, nonlinear effect on GDP growth, with an inverted-U shape peaking around 13–17°C depending on specification. The effect is identified from plausibly exogenous within-country temperature variation and is robust to alternative growth measures.

For countries in the tropics, these findings are not just academic. They suggest that historical temperature patterns have already been shaping economic outcomes — and that future warming will compound existing disadvantages. Designing effective climate adaptation policy requires understanding not just global averages, but where on the curve each country already sits.

References

Burke, Marshall, Solomon M. Hsiang, and Edward Miguel. 2015. “Global Non-Linear Effect of Temperature on Economic Production.” Nature 527 (7577): 235–39. https://doi.org/10.1038/nature15725.