Skip to content

Repository files navigation

onet2r onet2r hex logo

onet2r is an R package for working with O*NET Web Services, archived O*NET database releases, O*NET-SOC taxonomy bridges, BLS OEWS wage and employment context, and reproducible user-supplied occupation measures.

The package is built for analysts who need tidy current-release O*NET data and for researchers who need to ask careful historical questions. O*NET was not designed as a longitudinal panel, so onet2r makes the plumbing visible: archive versions, taxonomy seams, source dates, employment weights, coverage, and provenance.

Installation

You can install onet2r from CRAN after release:

install.packages("onet2r")

You can install the development version from GitHub:

# install.packages("pak")
pak::pak("farach/onet2r")

Authentication

Live O*NET API calls require a free API key from https://services.onetcenter.org/developer/. Store it in .Renviron:

ONET_API_KEY=your-api-key-here

The archive, OEWS, and measure examples below run without a key.

Read Archived Releases

archive_base <- system.file("extdata", "onet-mini", package = "onet2r")
archives <- c(
  `24.3` = file.path(archive_base, "db_24_3_text"),
  `25.1` = file.path(archive_base, "db_25_1_text")
)
release_dates <- c(`24.3` = "2020-08-01", `25.1` = "2020-11-01")

abilities <- onet_panel(
  "Abilities",
  versions = c("24.3", "25.1"),
  scale = "IM",
  archives = archives,
  release_dates = release_dates
)

abilities |>
  select(release_version, onet_soc_code, soc_code, element_name, data_value) |>
  head(6) |>
  knitr::kable(digits = 3, align = "l")
release_version onet_soc_code soc_code element_name data_value
24.3 15-1132.00 15-1132 Oral Comprehension 4.20
24.3 15-1132.00 15-1132 Problem Sensitivity 4.40
24.3 29-1141.00 29-1141 Oral Comprehension 4.60
25.1 15-1252.00 15-1252 Oral Comprehension 4.48
25.1 15-1253.00 15-1253 Oral Comprehension 4.30
25.1 29-1141.00 29-1141 Oral Comprehension 4.66

O*NET-SOC remains at the native 8-digit detail level in onet_soc_code. The 6-digit soc_code exists for labor-market joins.

Reconcile Historical Change

bridge_2010_2019 <- tibble::tibble(
  from_vintage = "2010",
  to_vintage = "2019",
  from_onet_soc_code = c("15-1132.00", "15-1132.00", "29-1141.00"),
  to_onet_soc_code = c("15-1252.00", "15-1253.00", "29-1141.00"),
  map_type = c("split", "split", "one_to_one"),
  crosswalk_weight = c(0.5, 0.5, 1)
)

changes <- onet_panel_reconcile(
  abilities,
  bridge = bridge_2010_2019
)

changes |>
  select(
    from_onet_soc_code,
    to_onet_soc_code,
    element_name,
    from_value,
    to_value,
    value_change,
    change_type,
    safely_comparable
  ) |>
  arrange(desc(abs(value_change))) |>
  head(8) |>
  knitr::kable(digits = 3, align = "l")
from_onet_soc_code to_onet_soc_code element_name from_value to_value value_change change_type safely_comparable
15-1132.00 15-1252.00 Oral Comprehension 4.2 4.48 0.28 transition_data FALSE
15-1132.00 15-1253.00 Oral Comprehension 4.2 4.30 0.10 transition_data FALSE
29-1141.00 29-1141.00 Oral Comprehension 4.6 4.66 0.06 real_update TRUE
15-1132.00 15-1252.00 Problem Sensitivity 4.4 NA NA dropped FALSE
15-1132.00 15-1253.00 Problem Sensitivity 4.4 NA NA dropped FALSE
changes |>
  count(change_type, name = "rows") |>
  arrange(desc(rows)) |>
  knitr::kable(digits = 3, align = "l")
change_type rows
transition_data 2
dropped 2
real_update 1

Rows marked as transition data, suppressed estimates, new content, or dropped content are visible in change_type. They are not counted as safely comparable updates.

Bring Your Own Measure

The package does not ship an AI exposure score or any other substantive measure. You supply a score, and onet2r validates keys, performs mechanical rollups, adds weights, and records provenance.

tasks <- onet_archive_read(
  "30.3",
  "Task Statements",
  path = file.path(archive_base, "db_30_3_text"),
  release_date = "2026-05-01"
)
task_ratings <- onet_archive_read(
  "30.3",
  "Task Ratings",
  path = file.path(archive_base, "db_30_3_text"),
  release_date = "2026-05-01"
)

task_scores <- tibble::tibble(
  task_id = c("1001", "1002", "2001"),
  score = c(0.80, 0.40, 0.20)
)

measure <- onet_measure(
  task_scores,
  key = "task_id",
  score = "score",
  key_type = "task",
  universe = tasks$task_id,
  measure_id = "stylized_task_score",
  release_version = "30.3"
)

onet_coverage(measure) |>
  knitr::kable(digits = 3, align = "l")
key_type n_input n_universe n_matched coverage_share employment_coverage_share
task 3 3 3 1 NA
occupation_scores <- onet_task_to_occupation(
  measure,
  task_ratings = task_ratings,
  task_metadata = tasks,
  include_supplemental = FALSE
)

occupation_scores |>
  select(onet_soc_code, soc_code, n_tasks, total_task_weight, measure_score) |>
  knitr::kable(digits = 3, align = "l")
onet_soc_code soc_code n_tasks total_task_weight measure_score
15-1252.00 15-1252 1 95 0.8
29-1141.00 29-1141 1 98 0.2

Add Employment Weights

If BLS rejects an automated OEWS ZIP download, onet_oews_national() can use a matching ZIP that you downloaded in your browser. Leave the ZIP in your Downloads folder, or set options(onet2r.oews_download_dir = "path/to/downloads"). Passing path is always supported, keeps examples offline, and remains the fallback if BLS changes OEWS URLs or file names.

oews_sample <- onet_oews_national(
  path = system.file("extdata", "oews-national-sample.csv", package = "onet2r")
)

weights <- onet_weight_panel_oews(oews_sample, year = 2024)
#> Dropped 2 OEWS aggregate rows; keeping "detailed" occupations.

weights |>
  knitr::kable(digits = 3, align = "l")
reference_soc_code year employment weight_share source source_taxonomy reference_taxonomy
11-1011 2024 211230 0.040 OEWS 2018 SOC 2018 SOC
15-1252 2024 1847900 0.353 OEWS 2018 SOC 2018 SOC
29-1141 2024 3175400 0.607 OEWS 2018 SOC 2018 SOC
aggregate <- onet_measure_aggregate(
  occupation_scores,
  weights
)

aggregate |>
  select(-coverage, -provenance) |>
  knitr::kable(digits = 3, align = "l")
measure_id aggregate total_employment covered_employment employment_coverage_share n_occupations n_reference_soc
stylized_task_score 0.421 5234530 5023300 0.96 2 2
onet_provenance(aggregate) |>
  knitr::kable(digits = 3, align = "l")
measure_id measure_release weight_source weight_year source_taxonomy reference_taxonomy bridge_used crosswalk_path
stylized_task_score 30.3 OEWS 2024 2018 SOC 2018 SOC FALSE 2018 SOC -> 2018 SOC

Stress Test the Plumbing

pums_weights <- onet_weight_panel_pums(
  tibble::tibble(
    SOCP = c("151252", "151252", "291141", "291141"),
    PWGTP = c(80, 120, 200, 80)
  ),
  year = 2022
)

sensitivity <- onet_measure_sensitivity(
  measure,
  weight_panels = list(oews = weights, pums = pums_weights),
  task_ratings = task_ratings,
  task_metadata = tasks,
  include_supplemental = c(FALSE, TRUE)
)

sensitivity |>
  select(
    scenario,
    aggregate,
    employment_coverage_share,
    movement,
    movement_percent
  ) |>
  knitr::kable(digits = 3, align = "l")
scenario aggregate employment_coverage_share movement movement_percent
RT_core / task_release / oews / no_bridge 0.421 0.96 0.000 0.000
RT_core / task_release / pums / no_bridge 0.450 1.00 0.029 0.070
RT_core_plus_supplemental / task_release / oews / no_bridge 0.373 0.96 -0.047 -0.112
RT_core_plus_supplemental / task_release / pums / no_bridge 0.396 1.00 -0.024 -0.058

Decompose Aggregate Change

from_scores <- tibble::tibble(
  reference_soc_code = c("15-1252", "29-1141"),
  measure_score = c(1.0, 2.0),
  safely_comparable = c(TRUE, FALSE)
)
to_scores <- tibble::tibble(
  reference_soc_code = c("15-1252", "29-1141"),
  measure_score = c(2.0, 2.5),
  safely_comparable = c(TRUE, FALSE)
)
from_weights <- tibble::tibble(
  reference_soc_code = c("15-1252", "29-1141"),
  employment = c(100, 100)
)
to_weights <- tibble::tibble(
  reference_soc_code = c("15-1252", "29-1141"),
  employment = c(150, 50)
)

decomp <- onet_decompose_change(from_scores, to_scores, from_weights, to_weights)

decomp |>
  select(component, value) |>
  knitr::kable(digits = 3, align = "l")
component value
within 0.500
between -0.250
interaction 0.125
unclassifiable 0.250
total_change 0.625
onet_coverage(decomp) |>
  knitr::kable(digits = 3, align = "l")
n_common n_safely_comparable leakage
2 1 0

Main Function Groups

  • Current O*NET API data: onet_search(), onet_occupation(), onet_skills(), onet_tasks(), onet_table().
  • Archived O*NET data: onet_releases(), onet_archive_download(), onet_archive_read(), onet_panel(), onet_panel_reconcile().
  • Wage and employment context: onet_oews_national(), onet_weight_panel_oews(), onet_weight_panel_pums().
  • User-measure plumbing: onet_measure(), onet_task_to_occupation(), onet_measure_aggregate(), onet_measure_sensitivity(), onet_provenance(), onet_coverage(), onet_decompose_change().

Related tools

  • ONETr (CRAN) wraps the retired O*NET Web Services v1 XML API with username/password auth; it predates the v2 API this package targets.
  • blscrapeR / blsAPI wrap the BLS time-series API, which does not serve the OEWS research flat files onet2r ingests; they are complementary, not overlapping.
  • tidycensus / ipumsr are the right tools for fetching ACS PUMS microdata; onet2r deliberately has no Census fetcher and consumes their output for employment weights.
  • No other R or Python tool assembles longitudinal panels from historical O*NET database releases; that machinery is unique to onet2r.

About

No description, website, or topics provided.

Resources

Stars

5 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages