Skip to contents

Introduction

When you compute a statistic from survey data—say, the unemployment rate is 7.2%—that number alone doesn’t tell you much. Is 7.2% meaningfully different from last month’s 7.0%? Without standard errors and confidence intervals, you can’t answer this question.

This matters especially when tracking changes over time. Monthly data from PNADCperiods reveals dynamics hidden in quarterly averages, but those sharp month-to-month movements could be real economic changes or just sampling variability. Standard errors let you distinguish signal from noise.

The challenge is that PNADC uses a complex sample design—stratification (Estrato), clustering (UPA), and unequal probabilities—which means standard formulas underestimate variance. This vignette shows two strategies for proper variance estimation:

  1. Linearization: Using design variables directly—simple and fast
  2. Replication weights: Adjusting IBGE’s 200 bootstrap weights—more intensive but exact

We demonstrate both using the gender gap in labor force participation, which widened dramatically during COVID-19.


Why Standard Errors Matter

Before diving into methodology, consider the figure below. It shows labor force participation for men and women during COVID-19. The shaded bands are 95% confidence intervals from proper complex survey analysis.

Show plotting code
# Subset to COVID period for focused visualization
p3_data <- results_by_sex[period >= "2019-01-01" & period <= "2022-06-01"]

p3 <- ggplot(p3_data, aes(x = period, y = participation, color = sex)) +

  # Just the lines (no CI)
  geom_line(linewidth = 0.9) +

  # Add confidence bands with low alpha
  geom_ribbon(aes(ymin = ci_l, ymax = ci_u, fill = sex),
              alpha = 0.25, color = NA) +

  # Scales and labels
  scale_y_continuous(
    labels = percent_format(accuracy = 1),
    limits = c(0.45, 0.80)
  ) +
  scale_x_date(date_breaks = "6 months", date_labels = "%b\n%Y") +
  scale_color_manual(values = c("Men" = "#0074D9", "Women" = "#FF4136")) +
  scale_fill_manual(values = c("Men" = "#0074D9", "Women" = "#FF4136")) +

  # Add annotations for interpretation
  annotate("segment",
           x = as.Date("2020-06-15"), xend = as.Date("2020-06-15"),
           y = 0.52, yend = 0.62,
           arrow = arrow(ends = "both", length = unit(0.1, "cm")),
           color = "gray30") +
  annotate("text",
           x = as.Date("2020-06-15"), y = 0.50,
           label = "Is this drop\nstatistically\nsignificant?",
           size = 2.8, color = "gray30") +

  labs(
    title = "Why Confidence Intervals Matter: COVID-19 Labor Market Shock",
    subtitle = "Confidence bands (shaded) show the precision of estimates. Overlapping bands suggest uncertain differences.",
    x = NULL,
    y = "Labor Force Participation Rate",
    color = "Sex",
    fill = "Sex",
    caption = "Source: PNADC/IBGE. Confidence bands show +/- 1.96 SE from complex survey design."
  ) +

  theme_minimal(base_size = 11) +
  theme(
    plot.title = element_text(face = "bold"),
    legend.position = "bottom",
    panel.grid.minor = element_blank()
  )
p3
Why Confidence Intervals Matter: COVID-19 Labor Market Shock
Why Confidence Intervals Matter: COVID-19 Labor Market Shock

Without confidence intervals, we might over-interpret month-to-month fluctuations as real economic changes. With them, we can assess which movements are statistically meaningful.


Prerequisites

# Core packages
library(PNADCperiods)
library(data.table)
library(fst)

# Survey analysis
library(survey)

# Visualization
library(ggplot2)
library(scales)

Loading and Preparing Data

# Load stacked PNADC quarterly data (all years available)
# Requires: identification vars, calibration vars, Estrato, plus analysis vars
files <- list.files("D:/Dropbox/Bancos_Dados/PNADC/Trimestral/Dados/",
                    pattern = "^pnadc_\\d{4}-\\dq\\.fst$", full.names = TRUE)

cols_needed <- c(
  # Mensalization keys
  "Ano", "Trimestre", "UPA", "V1008", "V1014",
  # Birthday variables (for identification)
  "V2008", "V20081", "V20082", "V2009",
  # Calibration variables
  "V1028", "UF", "posest", "posest_sxi",
  # Survey design variables
  "Estrato",
  # Analysis variables
  "VD4001", "VD4002", "V2007"
)

pnadc <- rbindlist(lapply(files, function(f) {
  fst::read_fst(f, as.data.table = TRUE, columns = cols_needed)
}), fill = TRUE)

# Step 1: Build crosswalk (identify reference periods)
crosswalk <- pnadc_identify_periods(pnadc, verbose = TRUE)

# Step 2: Apply crosswalk and calibrate weights
pnadc <- pnadc_apply_periods(pnadc, crosswalk,
                              weight_var = "V1028",
                              anchor = "quarter",
                              calibrate = TRUE,
                              calibration_unit = "month",
                              verbose = TRUE)

# Filter to determined observations with monthly weights
pnadc_monthly <- pnadc[!is.na(weight_monthly)]

# Handle singleton strata globally
options(survey.lonely.psu = "adjust")

Strategy 1: Linearization with Strata and PSU

This approach uses Taylor series linearization to estimate variances. PNADC uses a two-stage stratified cluster sample: within each stratum (Estrato), Primary Sampling Units (UPA) are selected with probability proportional to size, then households within selected UPAs are interviewed.

Setting Up a Survey Design

# Create survey design object for a specific month (e.g., January 2020)
pnadc_jan2020 <- pnadc_monthly[ref_month_yyyymm == 202001 & V2009 >= 14]

design_jan2020 <- svydesign(
  ids = ~UPA,                    # PSU/cluster identifier
  strata = ~Estrato,             # Stratification variable
  weights = ~weight_monthly,     # Monthly-adjusted weight
  data = pnadc_jan2020,
  nest = TRUE                    # UPAs nested within strata
)

# Labor force participation rate
svymean(~I(VD4001 == 1), design_jan2020, na.rm = TRUE)
confint(svymean(~I(VD4001 == 1), design_jan2020, na.rm = TRUE))

Monthly Time Series with Confidence Intervals

To create a full monthly series, loop over months with a function that handles edge cases:

# Prepare analysis data
pnadc_monthly[, V2009 := as.integer(V2009)]
pnadc_analysis <- pnadc_monthly[V2009 >= 14]
pnadc_analysis[, VD4001 := as.integer(VD4001)]
pnadc_analysis[, in_labor_force := fifelse(VD4001 == 1, 1L, 0L)]
pnadc_analysis[, V2007 := as.integer(V2007)]
pnadc_analysis[, sex := fifelse(V2007 == 1, "Men", "Women")]

# Function to compute participation by sex for one month
compute_participation_by_sex <- function(month_yyyymm, data) {

  month_data <- data[ref_month_yyyymm == month_yyyymm]

  if (nrow(month_data) < 100) return(NULL)

  tryCatch({
    design <- svydesign(
      ids = ~UPA,
      strata = ~Estrato,
      weights = ~weight_monthly,
      data = month_data,
      nest = TRUE
    )

    result <- svyby(
      ~in_labor_force,
      by = ~sex,
      design = design,
      FUN = svymean,
      na.rm = TRUE,
      vartype = c("se", "ci")
    )

    setDT(result)
    result[, ref_month_yyyymm := month_yyyymm]
    result
  }, error = function(e) {
    cat("Error processing month", month_yyyymm, ":", e$message, "\n")
    NULL
  })
}

# Compute for all months
months <- sort(unique(pnadc_analysis$ref_month_yyyymm))
results_by_sex <- rbindlist(
  lapply(months, compute_participation_by_sex, data = pnadc_analysis)
)

# Rename estimate column for clarity
setnames(results_by_sex, "in_labor_force", "participation")

# Add date column for plotting
results_by_sex[, period := as.Date(paste0(
  ref_month_yyyymm %/% 100, "-",
  ref_month_yyyymm %% 100, "-15"
))]

The tryCatch wrapper handles months where the survey design might fail (e.g., sparse data in early pandemic months). The svyby() function returns columns participation, se, ci_l, and ci_u.


Strategy 2: Replication Weights

IBGE provides 200 bootstrap replication weights (V1028001 to V1028200) with each quarterly release. This approach is more computationally intensive (201 computations per statistic) but handles complex statistics like quantiles, Gini coefficients, and regression coefficients without special variance formulas.

The key insight: apply the same calibration ratio to each replication weight:

V1028kmonthly=V1028k×weight_monthlyV1028 \text{V1028}_k^{\text{monthly}} = \text{V1028}_k \times \frac{\text{weight\_monthly}}{\text{V1028}}

# Calculate the adjustment ratio
pnadc_monthly[, weight_ratio := weight_monthly / V1028]

# Get names of replication weight columns
rep_weight_cols <- paste0("V1028", sprintf("%03d", 1:200))

# Create adjusted replication weight columns
for (col in rep_weight_cols) {
  new_col <- paste0(col, "_monthly")
  pnadc_monthly[, (new_col) := get(col) * weight_ratio]
}
# Create replicate weights design (e.g., for January 2020)
pnadc_jan2020_rep <- pnadc_monthly[ref_month_yyyymm == 202001 & V2009 >= 14]
rep_weight_cols_monthly <- paste0("V1028", sprintf("%03d", 1:200), "_monthly")

design_rep <- svrepdesign(
  data = pnadc_jan2020_rep,
  weights = ~weight_monthly,
  repweights = rep_weight_cols_monthly,
  type = "bootstrap",
  combined.weights = TRUE
)

# Use the same svymean/svyby functions as with linearization
svymean(~I(VD4001 == 1), design_rep, na.rm = TRUE)

Comparing Strategies

Aspect Linearization Replication
Setup Simpler (strata + PSU) More complex (200 weight columns)
Speed Fast Slower (200x calculations)
Complex statistics May need special formulas Handles automatically
Best for Routine analysis Quantiles, Gini, publications

Recommendation: Use linearization for exploratory analysis and most routine work. Use replication when computing complex statistics or preparing results for publication.


Practical Example: Gender Gap in Labor Force Participation

Brazil has one of the largest gender gaps in labor force participation among major economies—about 20 percentage points. COVID-19 provides a natural experiment: school closures and increased caregiving demands fell disproportionately on women. Monthly data lets us track whether the gap widened, and confidence intervals tell us if the change was statistically significant.

Computing the Gender Gap

The gap is a derived statistic: the difference between two survey estimates. We derive it from the by-sex results already computed above:

# Reshape to wide format to compute gap
results_wide <- dcast(results_by_sex, ref_month_yyyymm + period ~ sex,
                      value.var = c("participation", "se"))

# Compute gap and its SE
# SE of difference: SE(gap) = sqrt(SE_men^2 + SE_women^2)
results_wide[, `:=`(
  gap = participation_Men - participation_Women,
  gap_se = sqrt(se_Men^2 + se_Women^2)
)]

results_wide[, `:=`(
  gap_ci_lower = gap - 1.96 * gap_se,
  gap_ci_upper = gap + 1.96 * gap_se
)]

Visualizing the Results

Show plotting code
# Reshape back to long for plotting
results_long <- results_by_sex[!is.na(participation)]

# Ensure proper CI columns exist
if (!"ci_l" %in% names(results_long)) {
  results_long[, ci_l := participation - 1.96 * se]
  results_long[, ci_u := participation + 1.96 * se]
}

p1 <- ggplot(results_long, aes(x = period, y = participation, color = sex, fill = sex)) +

  # Confidence bands
  geom_ribbon(aes(ymin = ci_l, ymax = ci_u), alpha = 0.2, color = NA) +

  # Point estimates
  geom_line(linewidth = 0.8) +

  # Highlight COVID period
  annotate("rect",
           xmin = as.Date("2020-03-01"), xmax = as.Date("2021-12-31"),
           ymin = -Inf, ymax = Inf,
           fill = "gray80", alpha = 0.3) +
  annotate("text",
           x = as.Date("2021-01-01"), y = 0.85,
           label = "COVID-19", fontface = "italic", size = 3, color = "gray40") +

  # Scales and labels
  scale_y_continuous(
    labels = percent_format(accuracy = 1),
    limits = c(0.45, 0.85),
    breaks = seq(0.45, 0.85, 0.05)
  ) +
  scale_x_date(date_breaks = "1 year", date_labels = "%Y") +
  scale_color_manual(values = c("Men" = "#0074D9", "Women" = "#FF4136")) +
  scale_fill_manual(values = c("Men" = "#0074D9", "Women" = "#FF4136")) +

  labs(
    title = "Labor Force Participation by Sex: Monthly Series with 95% Confidence Intervals",
    subtitle = "Brazil, 2012-2025. Shaded regions show 95% confidence bands from complex survey design.",
    x = NULL,
    y = "Labor Force Participation Rate",
    color = "Sex",
    fill = "Sex",
    caption = "Source: PNADC/IBGE. Monthly weights from PNADCperiods. Variance estimated via Taylor linearization."
  ) +

  theme_minimal(base_size = 11) +
  theme(
    plot.title = element_text(face = "bold"),
    legend.position = "bottom",
    panel.grid.minor = element_blank()
  )
p1
Labor Force Participation by Sex with 95% Confidence Intervals
Labor Force Participation by Sex with 95% Confidence Intervals

The confidence bands for men and women never overlap, confirming that the gender gap is statistically significant in every month. Women’s participation dropped more sharply during COVID-19 (from ~53% to below 47%) than men’s (from ~73% to ~66%).

Show plotting code
# Pre-COVID average for reference line
pre_covid_avg <- results_wide[period < "2020-03-01", mean(gap, na.rm = TRUE)]

p2 <- ggplot(results_wide[!is.na(gap)], aes(x = period, y = gap)) +

  # Confidence band
  geom_ribbon(aes(ymin = gap_ci_lower, ymax = gap_ci_upper),
              fill = "#7b3294", alpha = 0.3) +

  # Point estimate
  geom_line(color = "#7b3294", linewidth = 0.9) +

  # Reference line at pre-COVID average
  geom_hline(
    yintercept = pre_covid_avg,
    linetype = "dashed", color = "gray50"
  ) +
  annotate("text", x = min(results_wide$period, na.rm = TRUE) + 180,
           y = pre_covid_avg + 0.008,
           label = paste0("Pre-COVID avg: ", sprintf("%.1f", pre_covid_avg * 100), " p.p."),
           size = 3, color = "gray40", hjust = 0) +

  # Highlight COVID period
  annotate("rect",
           xmin = as.Date("2020-03-01"), xmax = as.Date("2021-12-31"),
           ymin = -Inf, ymax = Inf,
           fill = "gray80", alpha = 0.3) +

  # Scales and labels
  scale_y_continuous(
    labels = percent_format(accuracy = 1),
    breaks = seq(0.15, 0.30, 0.025)
  ) +
  scale_x_date(date_breaks = "1 year", date_labels = "%Y") +

  labs(
    title = "Gender Gap in Labor Force Participation (Men - Women)",
    subtitle = "Monthly series with 95% CI. Dashed line = pre-COVID average. Gap widened during pandemic.",
    x = NULL,
    y = "Participation Gap (percentage points)",
    caption = "Source: PNADC/IBGE. Monthly weights from PNADCperiods."
  ) +

  theme_minimal(base_size = 11) +
  theme(
    plot.title = element_text(face = "bold"),
    panel.grid.minor = element_blank()
  )
p2
Gender Gap in Labor Force Participation
Gender Gap in Labor Force Participation

The gap widened from ~20 to ~23 percentage points during COVID-19, and the confidence bands confirm this increase was statistically significant. The gap returned to pre-pandemic levels by 2022.


Practical Tips

Singleton Strata

Some months may have strata with only one PSU. The options(survey.lonely.psu = "adjust") setting (shown in the data preparation section above) provides a conservative adjustment. Other options include "remove" and "average".

Subpopulation Analysis

When analyzing subgroups, always subset the design object, not the raw data:

# CORRECT: Subset the design
design_women <- subset(design_jan2020, V2007 == 2)
svymean(~I(VD4001 == 1), design_women, na.rm = TRUE)

# INCORRECT: Don't do this - loses design information
# bad_design <- svydesign(..., data = pnadc[V2007 == 2])

Subsetting the data before creating the design object loses information about the full sample structure, leading to incorrect variance estimates.


Summary

This vignette demonstrated two approaches for computing standard errors with monthly PNADC weights:

  1. Linearization: Use Estrato as strata, UPA as PSU, and weight_monthly as weight in svydesign(). Fast, simple, works for most statistics.
  2. Replication: Multiply each bootstrap weight by weight_monthly / V1028. More intensive but exact for complex statistics.

The gender gap example demonstrated that confidence intervals transform a claim like “the gap looks bigger” into “the gap increased by 2-3 percentage points, and we’re 95% confident it’s real.”


References

  • HECKSHER, Marcos. “Valor Impreciso por Mes Exato: Microdados e Indicadores Mensais Baseados na Pnad Continua”. IPEA - Nota Tecnica Disoc, n. 62. Brasilia, DF: IPEA, 2020. https://portalantigo.ipea.gov.br/portal/index.php?option=com_content&view=article&id=35453
  • HECKSHER, M. “Cinco meses de perdas de empregos e simulacao de um incentivo a contratacoes”. IPEA - Nota Tecnica Disoc, n. 87. Brasilia, DF: IPEA, 2020.
  • HECKSHER, Marcos. “Mercado de trabalho: A queda da segunda quinzena de marco, aprofundada em abril”. IPEA - Carta de Conjuntura, v. 47, p. 1-6, 2020.
  • Thomas Lumley (2010). Complex Surveys: A Guide to Analysis Using R. Wiley.
  • IBGE. Notas Metodologicas da PNAD Continua.
  • Barbosa, Rogerio J; Hecksher, Marcos. (2026). PNADCperiods: Identify Reference Periods in Brazil’s PNADC Survey Data. R package version v0.1.0. https://github.com/antrologos/PNADCperiods

Further Reading