Date: 2026-08-22

# Getting Started with lehdr

### Accessing and Analyzing LODES Data in R

#### Jamaal Green

#### Dillon Mahmoudi

#### Liming Wang

#### 22 August 2026

## Introduction

lehdr is an R package that provides programmatic access to the Longitudinal Employer-Household Dynamics (LEHD) Origin-Destination Employment Statistics (LODES) dataset. LODES forms the backbone of the U.S. Census Bureau’s OnTheMap web application, which maps commuting patterns at fine geographic scales. While OnTheMap is a useful exploratory tool, it does not support systematic comparisons over time or across geographies. lehdr fills that gap by exposing LODES downloads as tidy data frames and providing built-in functions for common analyses.

LODES contains three file types:

- OD (`lodes_type = "od"`): Origin-destination flows, giving the number of workers by home-workplace Census block pair.
- RAC (`lodes_type = "rac"`): Residential area characteristics, giving job totals at workers’ home blocks.
- WAC (`lodes_type = "wac"`): Workplace area characteristics, giving job totals at workers’ job blocks.

## Installation

    # From CRAN
    install.packages("lehdr")

    # Development version from GitHub
    devtools::install_github("jamgreen/lehdr")

    library(lehdr)
    library(dplyr)

## Basic Usage

### Downloading a single state and year

The following example retrieves 2020 Oregon OD data aggregated to the Census tract level. Only primary jobs (`job_type = "JT01"`) across all worker segments (`segment = "S000"`) are included.

    or_od <- grab_lodes(
      state      = "or",
      year       = 2020,
      lodes_type = "od",
      job_type   = "JT01",
      segment    = "S000",
      state_part = "main",
      agg_geo    = "tract"
    )

    head(or_od)

### Multiple states and years

Pass character vectors to `state` and/or numeric vectors to `year`. The function returns a single bound tibble across all combinations.

    or_ri_od <- grab_lodes(
      state      = c("or", "ri"),
      year       = c(2019, 2020),
      lodes_type = "od",
      job_type   = "JT01",
      segment    = "S000",
      state_part = "main",
      agg_geo    = "tract"
    )

    head(or_ri_od)

Not all state-year combinations are available for every LODES version. Consult the LEHD Technical Document for availability.

### Aggregation levels

`agg_geo` controls the output geography. Valid values are `"block"` (default), `"bg"` (block group), `"tract"`, `"county"`, and `"state"`. Aggregation truncates GEOID strings and sums all numeric columns.

    md_rac_county <- grab_lodes(
      state      = "md",
      year       = 2019,
      lodes_type = "rac",
      job_type   = "JT01",
      segment    = "S000",
      agg_geo    = "county"
    )

    head(md_rac_county)

### Attaching spatial geometries

Set `geometry = TRUE` to retrieve Census geometries via the `tigris` package and attach them to the result. For `lodes_type = "rac"` or `"wac"`, the return value is an `sf` object. For `lodes_type = "od"`, two geometry columns (`h_geometry`, `w_geometry`) are added but the object remains a tibble — use `sf::st_as_sf()` to promote it.

    md_wac_sf <- grab_lodes(
      state      = "md",
      year       = 2019,
      lodes_type = "wac",
      job_type   = "JT01",
      segment    = "S000",
      agg_geo    = "county",
      geometry   = TRUE
    )

    plot(md_wac_sf["C000"])

### Geographic crosswalk

`grab_crosswalk()` downloads the LODES geographic crosswalk, which maps Census blocks to all higher-level geographies. Useful when you need custom aggregations not covered by `agg_geo`.

    vt_xwalk <- grab_crosswalk("vt")

    # LODES7 vintage crosswalk (2010 Census blocks)
    vt_xwalk_7 <- grab_crosswalk("vt", version = "LODES7")

## Analytical Functions

lehdr includes three functions for common LODES analyses. All examples below use Maryland, which has a rich and varied labor market geography: a large central city (Baltimore), dense inner suburbs, agricultural Eastern Shore counties, and strong federal-sector employment in the Washington suburbs.

### Commute flow statistics

`compute_commute_stats()` summarizes commute flows from an OD dataset into per-geography metrics: workers arriving (inflow), workers departing (outflow), internal flows, net flow, and the self-containment ratio. Self-containment is the share of employed residents who also work within the same geographic unit — a value close to 1 indicates a largely self-sufficient labor market, while values near 0 indicate heavy out-commuting.

    od_md <- grab_lodes(
      state      = "md",
      year       = 2019,
      lodes_type = "od",
      job_type   = "JT00",
      segment    = "S000",
      state_part = "main",
      agg_geo    = "county"
    )

    commute_md <- compute_commute_stats(od_md, agg_geo = "county")

    # Most self-contained counties
    commute_md %>%
      arrange(desc(self_containment)) %>%
      select(county, workers_in, workers_out, net_flow, self_containment) %>%
      head(10)

Net flow identifies counties that are net job importers (positive) versus net exporters (negative), a key indicator of job/housing imbalance. The map below shows self-containment at the Census tract level for Baltimore City, where the pattern reflects the city’s mix of dense employment centers and residential neighborhoods with limited local job access.

    # See data-raw/render_vignette_figures.R for the full figure code.
    # Requires: ggplot2, sf, tigris, scales

### Longitudinal change

`compute_lodes_change()` computes absolute and percentage change in any LODES variable between two years. The output can be wide (one row per geography, with columns for each variable’s change) or long (one row per geography-variable combination, suitable for `ggplot2`).

    wac_md_panel <- grab_lodes(
      state      = "md",
      year       = c(2010, 2019),
      lodes_type = "wac",
      job_type   = "JT00",
      segment    = "S000",
      agg_geo    = "county"
    )

    change_md <- compute_lodes_change(
      wac_md_panel,
      geo_col      = "w_county",
      base_year    = 2010,
      compare_year = 2019,
      variables    = c("C000", "CE01", "CE02", "CE03")
    )

    # Counties with the largest absolute job growth
    change_md %>%
      arrange(desc(C000_change)) %>%
      select(w_county, C000_base, C000_compare, C000_change, C000_pct_change) %>%
      head(10)

The map below shows the percent change in total jobs (`C000`) across Maryland counties from 2010 to 2019. The diverging palette highlights counties that gained jobs (blue) versus those that lost jobs (red) over the period.

    # See data-raw/render_vignette_figures.R for the full figure code.
    # Requires: ggplot2, sf, tigris, scales

Long format is useful for visualizing change across multiple variables at once:

    change_long <- compute_lodes_change(
      wac_md_panel,
      geo_col   = "w_county",
      output    = "long",
      variables = c("CE01", "CE02", "CE03")
    )

    # Suitable for use with ggplot2 facets or grouped geom_col()
    head(change_long)

### Earnings tier shares

`compute_earnings_share()` computes the distribution of jobs (or resident workers) across three monthly earnings tiers defined by LODES:

Column | Tier | Monthly earnings
--- | --- | ---
`CE01` | Low | Up to $1,250
`CE02` | Mid | $1,251 to $3,333
`CE03` | High | Above $3,333

Both WAC and RAC files include these columns; `type` controls only the geography prefix used for auto-detection (`w_` for WAC, `h_` for RAC).

    wac_md <- grab_lodes(
      state      = "md",
      year       = 2019,
      lodes_type = "wac",
      job_type   = "JT00",
      segment    = "S000",
      agg_geo    = "county"
    )

    earn_shares <- compute_earnings_share(wac_md, type = "wac", geo_col = "w_county")

    # Counties with the highest share of low-wage jobs
    earn_shares %>%
      arrange(desc(share_low)) %>%
      select(w_county, share_low, share_mid, share_high) %>%
      head(10)

Use `output = "long"` to get a format ready for stacked bar charts:

    earn_long <- compute_earnings_share(
      wac_md,
      type    = "wac",
      geo_col = "w_county",
      output  = "long"
    )

    head(earn_long)

The chart below shows the earnings tier composition for all Maryland counties in 2019, sorted by the share of low-wage jobs. Counties with a larger low-wage share tend to be more rural or tourism-dependent, while suburban counties closer to Washington, D.C. show higher shares of well-paid jobs.

    # See data-raw/render_vignette_figures.R for the full figure code.
    # Requires: ggplot2, tigris, scales

Combining `compute_earnings_share()` and `compute_lodes_change()` supports longitudinal tracking of wage polarization. Compute shares for each year separately, then join and difference:

    wac_panel <- grab_lodes(
      state      = "md",
      year       = c(2010, 2019),
      lodes_type = "wac",
      job_type   = "JT00",
      segment    = "S000",
      agg_geo    = "county"
    )

    share_2010 <- compute_earnings_share(
      dplyr::filter(wac_panel, year == 2010),
      type = "wac", geo_col = "w_county"
    )

    share_2019 <- compute_earnings_share(
      dplyr::filter(wac_panel, year == 2019),
      type = "wac", geo_col = "w_county"
    )

## Mapping LODES Data

The figures in this vignette were produced with `ggplot2` and Census geometries from the `tigris` package. The general pattern is:

1. Download LODES data with `grab_lodes()`.
2. Compute a derived variable with one of the analytical functions above.
3. Fetch geometries with `tigris::tracts()`, `tigris::counties()`, etc.
4. Join on the GEOID column and plot with `geom_sf()`.

The following example shows a simple flow-weighted job accessibility index for Baltimore City tracts. For each home tract, the metric sums the total number of workers flowing to all reachable work tracts — a proxy for labor market connectivity that can be computed entirely from LODES OD data.

    od_md <- grab_lodes(
      state      = "md",
      year       = 2019,
      lodes_type = "od",
      job_type   = "JT00",
      segment    = "S000",
      state_part = "main",
      agg_geo    = "tract"
    )

    accessibility <- od_md %>%
      dplyr::filter(startsWith(h_tract, "24510")) %>%
      dplyr::group_by(h_tract) %>%
      dplyr::summarise(
        n_dest          = dplyr::n_distinct(w_tract),
        total_reachable = sum(S000, na.rm = TRUE),
        .groups         = "drop"
      )

    head(accessibility)

`n_dest` captures the breadth of access (number of distinct job destinations a tract sends workers to), while `total_reachable` is a flow-weighted measure of aggregate connectivity. A full accessibility analysis typically incorporates travel times or distances (see packages such as `r5r` or `accessibility`); the LODES-only measure here is a useful starting point when network data are unavailable.

    # See data-raw/render_vignette_figures.R for the full figure code.
    # Requires: ggplot2, sf, tigris, scales

## Caching

By default, downloaded files are deleted after reading. Setting `use_cache = TRUE` retains files in the user-level cache directory (`tools::R_user_dir("lehdr", "cache")`), which avoids re-downloading when you call `grab_lodes()` repeatedly with the same parameters. You can also set this option globally for a session:

    options(lehdr_use_cache = TRUE)

The cache filename includes the LODES version (e.g., `lodes8_md_wac_S000_JT00_2019.csv.gz`), so switching between LODES versions will not serve stale data from a different vintage.

Upgrading from lehdr < 1.2.0? Earlier versions cached files without a version prefix (e.g., `md_wac_S000_JT00_2019.csv.gz`). Those files will never be matched by the new naming scheme and can be safely deleted. To clear the entire cache:

    cache_dir <- tools::R_user_dir("lehdr", "cache")
    list.files(cache_dir)          # inspect before deleting
    unlink(cache_dir, recursive = TRUE)

## Citation

If you use lehdr in published work, please cite it. Running `citation("lehdr")` will return a BibTeX entry. In Chicago author-date style:

> Green, Jamaal, Liming Wang, and Dillon Mahmoudi. 2025. “lehdr: Grab Longitudinal Employer-Household Dynamics (LEHD) Flat Files.” R package version 1.2.0. https://github.com/jamgreen/lehdr/