Unemployment Claims Data

Unemployment Insurance Claims Replication

During the shutdown of the federal government, the U.S. Department of Labor (DOL) is not publishing weekly data on unemployment insurance claims1. However, the administrative data that is used in this process is still being produced by the states and published by the DOL on an automated basis, though with a slight delay2. Further, the DOL has previously published its weekly seasonal factors through March of 20263, enabling a reasonable recreation of the data that would typically be in in the weekly claims release. This report outlines a methodology to do so in R with the code included so that this process can be tested and repeated by others.

Packages

These are the packages used in this script. DETRLMI is a custom package used by the Research & Analysis Bureau in the Nevada Department of Employment, Training, and Rehabilitation. This package which includes a function that only downloads the new ETA 539 report from the DOL if it is different than the one currently cached by the user.

library(lubridate)
library(zoo)
library(DETRLMI)
library(data.table)
library(scales)
library(tidyverse)
library(readxl)
library(gt)

Getting DOL Seasonal Factors

Some code for this section is executed invisibly because it accesses a local document. To access this document, download the weekly claims data from the DOL through 2026 at https://oui.doleta.gov/unemploy/claims.asp. We then use read_excel to save this document as an object called dol_claims.

# dol_claims <- read_xlsx(**Local File Path**, skip = 2)

new_names <- c("reflecting", "ic_nsa", "ic_sf", "ic_sa", "ic_sa_4wk", "cc_nsa", "cc_sf", "cc_sa", "cc_sa_4wk", "iur_nsa", "iur_sa", "ce")
names(dol_claims) <- new_names

Getting Claim Data

Here we will download the non-embargoed version of the ETA 539 data from the DOL. This is typically released over the weekend following the Thursday when the claim data would typically be released. We use a custom function to avoid redundant downloads if the data has not been updated, but a simple call to data.table:fread("https://oui.doleta.gov/unemploy/csv/ar539.csv") would also work. We will be using C3 (regular UI initial claims), C4 (federal employee initial claims), C8 (regular UI weekly claims) and C9 (federal employee weekly claims) as this combination of cells appears to replicate what is used by the DOL.

path_539 <- DETRLMI::download_if_new("https://oui.doleta.gov/unemploy/csv/ar539.csv", check = "size")

ui_claims_raw <- fread(path_539) |>
  mutate(
    reflecting = as.Date(mdy(c2) + days(7))  # align timing with DOL
  )

Accounting for Missing Data

Because some states may not have reported data prior to the publication of this file, we will use the trailing 4-week average for any missing claim data to fill in the gaps, so that the aggregate totals will be unchanged.

# Calculate rolling averages
ui_claims_processed <- ui_claims_raw |>
  group_by(st) |>
  arrange(reflecting, .by_group = TRUE) |>
  mutate(
    c3_avg4 = rollapplyr(c3, width = 4, FUN = mean, fill = NA, na.rm = TRUE),
    c4_avg4 = rollapplyr(c4, width = 4, FUN = mean, fill = NA, na.rm = TRUE),
    c8_avg4 = rollapplyr(c8, width = 4, FUN = mean, fill = NA, na.rm = TRUE),
    c9_avg4 = rollapplyr(c9, width = 4, FUN = mean, fill = NA, na.rm = TRUE),
    c18_avg4 = rollapplyr(c18, width = 4, FUN = mean, fill = NA, na.rm = TRUE)
  ) |>
  ungroup()

# Fill missing states for last 4 weeks, working forward from t-3 to t
df <- ui_claims_processed
all_weeks <- sort(unique(df$reflecting))
latest_weeks <- tail(all_weeks, 4)  # t-3 through t

# Initialize tracking data frame
missing_states <- tibble(
  reflecting = as.Date(character()),
  missing_count = integer(),
  missing_states = character()
)

for (wk in latest_weeks) {
  wk <- as.Date(wk)
  existing_states <- df |> filter(reflecting == wk) |> pull(st)
  wk_missing <- setdiff(unique(df$st), existing_states)
  
  # If any states missing, fill them in
  if (length(wk_missing) > 0) {
    replacements <- df |>
      filter(st %in% wk_missing, reflecting < wk) |>
      group_by(st) |>
      arrange(desc(reflecting), .by_group = TRUE) |>
      slice_head(n = 1) |>
      mutate(
        reflecting = wk,
        c3 = c3_avg4,
        c4 = c4_avg4,
        c8 = c8_avg4,
        c9 = c9_avg4,
        c18 = c18_avg4
      ) |>
      ungroup()
    
    replacements$reflecting <- as.Date(replacements$reflecting)
    df$reflecting <- as.Date(df$reflecting)
    df <- bind_rows(df, replacements)
  }
  
  # Record missing states for this week
  missing_states <- bind_rows(
    missing_states,
    tibble(
      reflecting = wk,
      missing_count = length(wk_missing),
      missing_states = paste(wk_missing, collapse = ", ")
    )
  )
}

# Summarize and filter
ui_claims_download <- df |>
  group_by(reflecting) |>
  summarize(
    ic_no_x = sum(c3 + c4, na.rm = TRUE),
    cc_no_x = sum(c8 + c9, na.rm = TRUE),
    ce_admin = sum(c18, na.rm = TRUE), 
    .groups = "drop"
  ) |>
  filter(reflecting >= ymd("1990-01-01"))

The following table shows data for missing areas (U.S. states plus Washington, DC, Puerto Rico, and the U.S. Virgin Islands) at the time this report was generated. Due to delays in filing reports by these areas, not all states may be available when the report is generated. Estimates for missing areas may significantly affect the trend in claims data, so use caution in interpreting weeks with missing data for large states, several areas, or both.

missing_states |> 
  gt() |> 
  cols_label(
    reflecting = "Week Estimated",
    missing_count = "Number of Missing Areas",
    missing_states = "List of Missing Areas"
  ) |> 
  cols_align(
    align = "left",
    columns = everything()
  )
Week Estimated Number of Missing Areas List of Missing Areas
2025-10-04 1 VI
2025-10-11 1 VI
2025-10-18 1 VI
2025-10-25 4 AZ, DC, MA, VI

Apply Seasonal Factors and Chart Criteria

Here we will apply the seasonal factors from the DOL data and set a filter for the plot data to calendar year 2025. We also shift the weekly claim data to align with the DOL reporting periods.

plot_year <- 2025

combined_claims <- dol_claims |> 
  left_join(ui_claims_download) |> 
  mutate(
    cc_no_x_lead = lead(cc_no_x, 1),
    admin_ic_sa = ic_no_x / (ic_sf/100),
    admin_cc_sa = cc_no_x_lead / (cc_sf/100),
    admin_iur_sa = admin_cc_sa/ce_admin,
    admin_iur = cc_no_x_lead / ce_admin,
    admin_ic_sa_4wk = rollapplyr(admin_ic_sa, width = 4, FUN = mean, fill = NA, na.rm = TRUE),
    admin_cc_sa_4wk = rollapplyr(admin_cc_sa, width = 4, FUN = mean, fill = NA, na.rm = TRUE),
    admin_ic_py = lag(ic_no_x, 52),
    admin_cc_py = lag(cc_no_x_lead, 52),
    admin_iur_py = lag(admin_iur, 52)
    ) |> 
  filter(!is.na(ic_no_x))

Comparison to DOL Data

We can now plot the data as published by the DOL along with updates for report dates following October 1. These series are not identical, but are sufficiently similar to be a reasonable representation of what the DOL has previously published. Data since the start of the federal shutdown will, critically, not contain any manual or separate adjustments to the administrative data that the DOL would have made. Any adjustments for missing data or other one-time factors is not included in this data beyond those methods disclosed above.

One difference is that the official DOL release relies on two reports - the ETA 538 provides advance initial claims data, while the 539 provides initial claims and weekly claims data. This data set relies only on the revised ETA 539, so there are no revisions based on differing reports, only revisions to the reports themselves submitted by states.

Initial Claims

combined_claims |>
  mutate(reflecting = as.Date(reflecting)) |> 
  filter(year(reflecting) == plot_year) |>
  ggplot(aes(x = reflecting)) +
  geom_line(aes(y = admin_ic_sa), color = "navy", linewidth = 1, linetype = "solid") +
  geom_line(aes(y = ic_sa), color = "red", linewidth = 1, linetype = "dashed") +
  scale_y_continuous(labels = scales::comma) +
  scale_x_date(date_breaks = "1 month",
               date_labels = "%b-%d") +
  labs(title = "Weekly UI Claims Data",
       subtitle = "Red is data released by DOL, Blue is estimates based on ETA 539.",
       x = NULL, y = "Initial Claims", caption = "Source data: https://oui.doleta.gov/unemploy/csv/ar539.csv and https://oui.doleta.gov/unemploy/claims.asp") +
  theme_bw()

Comparison of generated and official seasonally adjusted initial claims data showing a close match between the two series.

Weekly Claims

combined_claims |>
  mutate(reflecting = as.Date(reflecting),
         cc_sa = if_else(cc_sa == 0, NA, cc_sa)) |> 
  filter(year(reflecting) == plot_year) |>
  ggplot(aes(x = reflecting)) +
  #geom_line(aes(y = cc_no_x_lead), color = "darkgreen", linewidth = 0.81, linetype = "solid") +
  geom_line(aes(y = admin_cc_sa), color = "navy", linewidth = 1, linetype = "solid") +
  geom_line(aes(y = cc_sa), color = "red", linewidth = 1, linetype = "dashed") +
  scale_y_continuous(labels = scales::comma) +
  scale_x_date(date_breaks = "1 month",
               date_labels = "%b-%d") +
  labs(title = "Weekly UI Claims Data",
       subtitle = "Red is data released by DOL, Blue is estimates based on ETA 539.",
       x = NULL, y = "Continued Claims", caption = "Source data: https://oui.doleta.gov/unemploy/csv/ar539.csv and https://oui.doleta.gov/unemploy/claims.asp") +
  theme_bw()

Comparison of generated and official seasonally adjusted weekly claims data showing a close match between the two series.

Replicated Weekly Release Contents

With this analysis in place, we can now produce content similar to that released by the Department of Labor each week4. These charts attempt to mirror the style and substance of the DOL data, in order to create some continuity in available economic data for the duration of the federal government shutdown. For consistency, this data will only use the DOL seasonal factors and claim counts from this reconstructed data set.

Seasonally Adjusted Data

start_date_ic <- format.Date(max(combined_claims$reflecting - weeks(52)), "%B %d, %Y")
end_date_ic <- format.Date(max(combined_claims$reflecting), "%B %d, %Y")
ic_title <- paste0(start_date_ic," - ",end_date_ic, " - shutdown period in grey")

shutdown_period <- combined_claims |> 
  filter(is.na(ic_sa),
         !is.na(admin_ic_sa)) |> 
  select(reflecting)

combined_claims |> 
  filter(reflecting >= max(reflecting) - weeks(52)) |> 
  select(reflecting, admin_ic_sa, admin_ic_sa_4wk) |> 
  pivot_longer(cols = admin_ic_sa:admin_ic_sa_4wk) |> 
  mutate(name = case_when(
    name == "admin_ic_sa" ~ "Weekly IC",
    name == "admin_ic_sa_4wk" ~ "Moving Average"
  )) |> 
  ggplot(aes(x = reflecting, y = value/1000, linetype = name, color = name)) +
      geom_rect(xmin = min(shutdown_period$reflecting), xmax = max(shutdown_period$reflecting), ymin = -Inf, ymax = Inf, fill = "#ddd", alpha = 0.2, color = NA) +
  geom_line(linewidth = 1) +
  labs(title = "Seasonally Adjusted Initial Claims",
       subtitle = ic_title,
       y = "Thousands", x = NULL, linetype = NULL, color = NULL) +
  scale_color_manual(values = c("firebrick3", "navy")) +
  theme_bw() +
  theme(
    plot.title = element_text(size = 14, hjust = 0.5),
    plot.subtitle = element_text(size = 12, hjust = 0.5),
    legend.position = "bottom",
    legend.justification = "right",
    legend.direction = "horizontal",
    legend.box = "horizontal",
    legend.background = element_rect(fill = "white", color = "black", size = 0.3),
    legend.key = element_blank(),
    #legend.margin = margin(t = 0, r = 0, b = 0, l = 10),
    legend.spacing.x = unit(0.4, "cm"),
    legend.text = element_text(size = 10),
    legend.title = element_blank(),
    plot.margin = margin(t = 10, r = 10, b = 10, l = 10)
  )

Seasonally Adjusted Initital Claims and 4-week moving average for the most recent 53 weeks.

start_date_cc <- format.Date(max(combined_claims$reflecting - weeks(53)), "%B %d, %Y")
end_date_cc<- format.Date(max(combined_claims$reflecting - weeks(1)), "%B %d, %Y")
cc_title <- paste0(start_date_cc," - ",end_date_cc, " - shutdown period in grey")

shutdown_period <- combined_claims |> 
  filter(is.na(cc_sa),
         !is.na(admin_cc_sa)) |> 
  select(reflecting)

combined_claims |> 
  filter(reflecting >= max(reflecting) - weeks(53),
         reflecting <= max(reflecting) - weeks(1)) |> 
  select(reflecting, admin_cc_sa, admin_cc_sa_4wk) |> 
  pivot_longer(cols = admin_cc_sa:admin_cc_sa_4wk) |> 
  mutate(name = case_when(
    name == "admin_cc_sa" ~ "Weekly IU",
    name == "admin_cc_sa_4wk" ~ "Moving Average"
  )) |> 
  ggplot(aes(x = reflecting)) +
  geom_rect(xmin = min(shutdown_period$reflecting), xmax = max(shutdown_period$reflecting), ymin = -Inf, ymax = Inf, fill = "#ddd", alpha = 0.2) +
  geom_line(aes(y = value/1000000, linetype = name, color = name), linewidth = 1) +
  labs(title = "Seasonally Adjusted Insured Unemployment",
       subtitle = cc_title,
       y = "Millions", x = NULL, linetype = NULL, color = NULL) +
  scale_color_manual(values = c("firebrick3", "navy")) +
  theme_bw() +
  theme(
    plot.title = element_text(size = 14, hjust = 0.5),
    plot.subtitle = element_text(size = 12, hjust = 0.5),
    legend.position = "bottom",
    legend.justification = "right",
    legend.direction = "horizontal",
    legend.box = "horizontal",
    legend.background = element_rect(fill = "white", color = "black", size = 0.3),
    legend.key = element_blank(),
    #legend.margin = margin(t = 0, r = 0, b = 0, l = 10),
    legend.spacing.x = unit(0.4, "cm"),
    legend.text = element_text(size = 10),
    legend.title = element_blank(),
    plot.margin = margin(t = 10, r = 10, b = 10, l = 10)
  )

Seasonally Adjusted Insured Unemployment and 4-week moving average for the most recent 53 weeks.

Unadjusted Data

start_date_ic <- format.Date(max(combined_claims$reflecting - weeks(104)), "%B %d, %Y")
end_date_ic <- format.Date(max(combined_claims$reflecting), "%B %d, %Y")
ic_title <- paste0(start_date_ic," - ",end_date_ic, " - shutdown period in grey")

shutdown_period <- combined_claims |> 
  filter(is.na(ic_sa),
         !is.na(admin_ic_sa)) |> 
  select(reflecting)

combined_claims |> 
  filter(reflecting >= max(reflecting) - weeks(52)) |> 
  select(reflecting, ic_no_x, admin_ic_py) |> 
  pivot_longer(cols = ic_no_x:admin_ic_py) |> 
  mutate(name = case_when(
    name == "ic_no_x" ~ "Latest 52 Weeks",
    name == "admin_ic_py" ~ "Prior Year"
  )) |> 
  ggplot(aes(x = reflecting, y = value/1000, linetype = name, color = name)) +
    geom_rect(xmin = min(shutdown_period$reflecting), xmax = max(shutdown_period$reflecting), ymin = -Inf, ymax = Inf, fill = "#ddd", alpha = 0.2, color = NA) +
  geom_line(linewidth = 1) +
  labs(title = "Not Seasonally Adjusted Initial Claims",
       subtitle = ic_title,
       y = "Thousands", x = NULL, linetype = NULL, color = NULL) +
  scale_color_manual(values = c("firebrick3", "navy")) +
  theme_bw() +
  theme(
    plot.title = element_text(size = 14, hjust = 0.5),
    plot.subtitle = element_text(size = 12, hjust = 0.5),
    legend.position = "bottom",
    legend.justification = "right",
    legend.direction = "horizontal",
    legend.box = "horizontal",
    legend.background = element_rect(fill = "white", color = "black", size = 0.3),
    legend.key = element_blank(),
    #legend.margin = margin(t = 0, r = 0, b = 0, l = 10),
    legend.spacing.x = unit(0.4, "cm"),
    legend.text = element_text(size = 10),
    legend.title = element_blank(),
    plot.margin = margin(t = 10, r = 10, b = 10, l = 10)
  )

Not Seasonally Adjusted Initital Claims and 4-week moving average for the most recent 53 weeks.

start_date_cc <- format.Date(max(combined_claims$reflecting - weeks(105)), "%B %d, %Y")
end_date_cc<- format.Date(max(combined_claims$reflecting - weeks(1)), "%B %d, %Y")
cc_title <- paste0(start_date_cc," - ",end_date_cc, " - shutdown period in grey")

shutdown_period <- combined_claims |> 
  filter(is.na(cc_sa),
         !is.na(admin_cc_sa)) |> 
  select(reflecting)

combined_claims |> 
  filter(reflecting >= max(reflecting) - weeks(53),
         reflecting <= max(reflecting) - weeks(1)) |> 
  select(reflecting, cc_no_x_lead, admin_cc_py) |> 
  pivot_longer(cols = cc_no_x_lead:admin_cc_py) |> 
  mutate(name = case_when(
    name == "cc_no_x_lead" ~ "Latest 52 Weeks",
    name == "admin_cc_py" ~ "Prior Year"
  )) |> 
  ggplot(aes(x = reflecting, y = value/1000000, linetype = name, color = name)) +
    geom_rect(xmin = min(shutdown_period$reflecting), xmax = max(shutdown_period$reflecting), ymin = -Inf, ymax = Inf, fill = "#ddd", alpha = 0.2, color = NA) +
  geom_line(linewidth = 1) +
  labs(title = "Not Seasonally Adjusted Insured Unemployment",
       subtitle = cc_title,
       y = "Millions", x = NULL, linetype = NULL, color = NULL) +
  scale_color_manual(values = c("firebrick3", "navy")) +
  theme_bw() +
  theme(
    plot.title = element_text(size = 14, hjust = 0.5),
    plot.subtitle = element_text(size = 12, hjust = 0.5),
    legend.position = "bottom",
    legend.justification = "right",
    legend.direction = "horizontal",
    legend.box = "horizontal",
    legend.background = element_rect(fill = "white", color = "black", size = 0.3),
    legend.key = element_blank(),
    #legend.margin = margin(t = 0, r = 0, b = 0, l = 10),
    legend.spacing.x = unit(0.4, "cm"),
    legend.text = element_text(size = 10),
    legend.title = element_blank(),
    plot.margin = margin(t = 10, r = 10, b = 10, l = 10)
  )

Not Seasonally Adjusted Insured Unemployment and 4-week moving average for the most recent 53 weeks.

Acknowledgements

This workforce product was funded by a grant awarded by the U.S. Department of Labor’s ETA. The product was created by the recipient and does not necessarily reflect the official position of the U.S. Department of Labor. The U.S. Department of Labor makes no guarantees, warranties, or assurances of any kind, express or implied, with respect to such information, including any information on linked sites and including, but not limited to, accuracy of the information or its completeness, timeliness, usefulness, adequacy, continued availability, or ownership. This product is copyrighted by the institution that created it. Internal use by an organization and/or personal use by an individual for non-commercial purposes is permissible. All other uses require the prior authorization of the copyright owner.

Produced by the Research & Analysis Bureau in the Nevada Department of Employment, Training, and Rehabilitation.

Report generated on October 30, 2025 at 01:29 PM