onet2r
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.
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" )
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.
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.
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
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
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
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().
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.