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-hereThe 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 |
OEWS publishes a few occupations only as combined codes, such as 31-1120 Home Health and Personal Care Aides. Build a bridge with onet_oews_bridge(occupation_scores, weights) and pass it as bridge to onet_measure_aggregate() so O*NET occupations inside those codes count toward coverage.
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_archive_reference(),onet_panel(),onet_panel_reconcile(). - Resurvey cycles and content change:
onet_resurvey_panel(),onet_condition_on_resurvey(),onet_content_change(). - Wage and employment context:
onet_oews_national(),onet_weight_panel_oews(),onet_oews_bridge(),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(). - Published exposure adapters:
onet_import_eloundou(),onet_import_felten_aioe().
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.
