Data collection last run: 2026-01-08
⚠️ This is not an official USAJobs project
| Year | Jobs Opened | Jobs Closed | Coverage Notes |
|---|---|---|---|
| 2013 | 5 | 0 | Very limited |
| 2014 | 24 | 19 | Very limited |
| 2015 | 140 | 131 | Very limited |
| 2016 | 3,879 | 1,633 | Very limited |
| 2017 | 237,145 | 226,248 | ✅ Complete year |
| 2018 | 328,111 | 315,729 | ✅ Complete year |
| 2019 | 349,256 | 336,608 | ✅ Complete year |
| 2020 | 327,545 | 315,161 | ✅ Complete year |
| 2021 | 369,151 | 352,375 | ✅ Complete year |
| 2022 | 441,604 | 419,295 | ✅ Complete year |
| 2023 | 454,652 | 434,527 | ✅ Complete year |
| 2024 | 367,774 | 352,303 | ✅ Complete year |
| 2025 | 234,093 | 224,368 | Current through January 08, 2026 |
| 2026 | 2,609 | 2,593 | Closing dates only |
Note: This table shows fields from historical jobs data. Current jobs API contains additional fields that are preserved in the original nested structure but not included in this rationalized view.
| Field | Type | Examples | Completeness |
|---|---|---|---|
HiringPaths |
JSON Array | [{"hiringPath": "Career transition (CTAP, ICTAP, RPL)"}, {"hiringPath": "The public"}], [{"hiringPath": "Internal to an agency"}] (75962 unique combinations) | 100% |
JobCategories |
JSON Array | [{"series": "1550"}, {"series": "0801"}, {"series": "1560"}], [{"series": "0260"}, {"series": "0501"}, {"series": "0560"}, {"series": "2210"}, {"series": "0340"}, {"series": "0201"}, {"series": "1801"}], [{"series": "2229"}], [{"series": "0850"}, {"series": "0810"}, {"series": "0808"}, {"series": "0801"}, {"series": "0830"}] (4424 unique) | 100% |
PositionLocations |
JSON Array | [{"positionLocationCity": "Highlands", "positionLocationState": "New Jersey", "positionLocationCountry": "United States"}, {"positionLocationCity": "Brooklyn", "positionLocationState": "New York", "positionLocationCountry": "United States"}], [{"positionLocationCity": "Pentagon, Arlington", "positionLocationState": "Virginia", "positionLocationCountry": "United States"}] (29707 unique combinations) | 100% |
agencyLevel |
Integer | 2, 1 | 100% |
agencyLevelSort |
String | Department of Defense\National Security Agency/Central Security Service, Department of Housing and Urban Development\Office of the Chief Information Officer, Department of the Air Force\Air Force Safety Center, Millennium Challenge Corporation (542 unique) | 100% |
announcementClosingTypeCode |
String | 01, 03, 02 | 100% |
announcementClosingTypeDescription |
String | Closing Date, Applicant Cut-Off, Open Continuous | 100% |
announcementNumber |
String | CARX-24-12591221-NH-DH, ST-12532350-24-JA, DE-12311052-24-KLW, DEST-12388565-24-JEL (363814 unique) | 100% |
appointmentType |
String | Agency Employees Only, Telework, Seasonal, Presidential Management Fellows (16 unique) | 100% |
backfilled |
String | True | 0% |
disableApplyOnline |
String | N, Y | 100% |
drugTestRequired |
String | N | 100% |
hiringAgencyCode |
String | ZS00, LC00, AR5A, AF4V (528 unique) | 100% |
hiringAgencyName |
String | Senate, Food and Nutrition Service, U.S. Army Recruiting Command and U.S. Army Cadet Command, Office of the Assistant Secretary for Policy (502 unique) | 99% |
hiringDepartmentCode |
String | PU, LF, CT, HE (122 unique) | 100% |
hiringDepartmentName |
String | Department of Homeland Security, Department of State, Department of Defense, Court Services and Offender Supervision Agency for DC (26 unique) | 100% |
hiringSubelementName |
String | Directorate of Plans, Training, Mobilization and Security, Vocational Rehabilitation and Employment (VR&E), Office of Airports, Southwest Region, Planning and Programming Branch, SW610, National Center for Science and Engineering Statistics (NCSES) (59972 unique) | 62% |
inserted_at |
String | 2025-07-05T13:52:56.627289, 2025-07-05T13:08:00.745389, 2025-09-20T11:55:10.928335, 2025-07-05T14:01:38.920074 (367190 unique) | 100% |
last_seen |
String | 2025-07-05T13:52:56.643980, 2025-09-20T11:51:24.311560, 2025-09-20T11:32:13.124947, 2025-07-05T13:05:06.412066 (367190 unique) | 100% |
maximumGrade |
String | KK, HI, HK, 71 (127 unique) | 100% |
maximumSalary |
Number | $0, $105,612, $470,281 (range: $0-$470,281) (15680 unique) | 100% |
minimumGrade |
String | 97, 24, M, 52 (125 unique) | 100% |
minimumSalary |
Number | $0, $72,553, $400,000 (range: $0-$400,000) (15417 unique) | 100% |
payScale |
String | WS, IM, RF, WD (191 unique) | 100% |
positionCloseDate |
String | 2024-02-13, 2025-10-21, 2025-11-04, 2025-02-25 (671 unique) | 100% |
positionExpireDate |
String | 2025-08-22, 2023-09-21, 2025-08-08, 2024-09-30 (596 unique) | 9% |
positionOpenDate |
String | 2024-10-12, 2024-09-30, 2024-06-03, 2024-04-18 (365 unique) | 100% |
positionOpeningStatus |
String | Candidate selected, Job canceled, Applications under review, Job closed (5 unique) | 100% |
positionTitle |
String | Law Enforcement Officer (School Resource Officer), Legal Instruments Examiner (Pre-Exam), ELEX MECH SUPV I, Diagnostic Radiologic Technologist-General/CT/Bone Densitometry (77991 unique) | 100% |
promotionPotential |
String | 22, 98, 25, XX (115 unique) | 100% |
relocationExpensesReimbursed |
String | N | 100% |
salaryType |
String | Per Hour, Per Year, Without Compensation, Per Day (9 unique) | 100% |
securityClearance |
String | Not Required, Secret, Other, Confidential (8 unique) | 100% |
securityClearanceRequired |
String | N, Y | 100% |
serviceType |
String | Competitive, Excepted, Senior Executive | 100% |
supervisoryStatus |
String | N, Y | 100% |
teleworkEligible |
String | N | 100% |
totalOpenings |
String | 5, 45, 131, 1498 (157 unique) | 90% |
travelRequirement |
String | Not required, Occasional travel, 25% or less, 50% or less (6 unique) | 100% |
usajobsControlNumber |
Integer | 767342200, 768473700, 769219600 (367190 unique) | 100% |
usajobs_control_number |
String | 767342200, 768473700, 769219600 (367190 unique) | 100% |
vendor |
String | USASTAFFING, Monster - Hiring Management, FAA - SWIFT, HQMC MCCS (PeopleSoft) (8 unique) | 100% |
whoMayApply |
String | Status Candidates (Merit Promotion and VEOA Eligibles) | 0% |
workSchedule |
String | Full-time, Intermittent, Part-time, Multiple Schedules (6 unique) | 100% |
This dataset combines data from two USAJobs APIs with field rationalization for consistent querying:
/api/historicjoa): Past job announcements by date range /api/Search): Currently active job postings Note: There is overlap between the APIs (2024-2025 jobs appear in both), but we collect from both APIs for completeness.
Important: The current_jobs_*.parquet files contain cumulative data - they include all jobs that have appeared in the Current API since we started collecting, not just jobs that are currently active. Once a job is added to these files, it remains there even after the position closes or is removed from the Current API. This provides a historical record of all jobs that were once "current."
| Historical Field Name | Historical API Source | Current API Source | Notes |
|---|---|---|---|
usajobsControlNumber |
usajobsControlNumber |
Extracted from PositionURI |
Numeric job identifier |
announcementNumber |
announcementNumber |
PositionID |
Public announcement ID |
hiringAgencyName |
hiringAgencyName |
DepartmentName |
Agency name |
hiringAgencyCode |
hiringAgencyCode |
OrganizationCodes (first part) |
Agency code |
positionTitle |
positionTitle |
PositionTitle |
Job title |
minimumGrade |
minimumGrade |
JobGrade[0].Code |
Minimum grade level |
maximumGrade |
maximumGrade |
JobGrade[-1].Code |
Maximum grade level |
minimumSalary |
minimumSalary |
PositionRemuneration[0].MinimumRange |
Minimum salary |
maximumSalary |
maximumSalary |
PositionRemuneration[0].MaximumRange |
Maximum salary |
positionOpenDate |
positionOpenDate |
PositionStartDate |
Position open date |
positionCloseDate |
positionCloseDate |
PositionEndDate |
Position close date |
usajobsControlNumber to identify records appearing in both APIs when neededHiringPaths, JobCategories, PositionLocations)MatchedObjectDescriptor, etc.)hiringAgencyName, positionTitle) while retaining access to original nested structuresThe examples.py script demonstrates both local file access and direct GitHub downloads. It runs complete analysis on 2.97M job postings and automatically cleans up downloaded files.
import pandas as pd
# Load a single year
df_2024 = pd.read_parquet('data/historical_jobs_2024.parquet')
print(f"✓ Successfully loaded {len(df_2024):,} job postings from 2024")
import pandas as pd
import requests
# Download directly from GitHub
url = 'https://github.com/abigailhaddad/usajobs_historical/raw/main/data/historical_jobs_2024.parquet'
df = pd.read_parquet(url)
print(f"✓ Downloaded {len(df):,} job postings")
# 1. TOP 15 HIRING AGENCIES (2024 DATA)
top_agencies = df_2024['hiringAgencyName'].value_counts().head(15)
for i, (agency, count) in enumerate(top_agencies.items(), 1):
percentage = count / len(df_2024) * 100
print(f"{i:2d}. {agency}: {count:,} jobs ({percentage:.1f}%)")
# Salary statistics
records_with_salary = df_2024[df_2024['maximumSalary'].notna()]
print(f"Records with salary data: {len(records_with_salary):,} ({len(records_with_salary)/len(df_2024)*100:.1f}%)")
# Salary ranges
min_range = f"${records_with_salary['minimumSalary'].min():,.0f} - ${records_with_salary['minimumSalary'].max():,.0f}"
max_range = f"${records_with_salary['maximumSalary'].min():,.0f} - ${records_with_salary['maximumSalary'].max():,.0f}"
median_range = f"${records_with_salary['minimumSalary'].median():,.0f} - ${records_with_salary['maximumSalary'].median():,.0f}"
print(f"Minimum salary range: {min_range}")
print(f"Maximum salary range: {max_range}")
print(f"Median salary range: {median_range}")
import duckdb
# Create DuckDB connection and register multiple Parquet files
conn = duckdb.connect('download/usajobs.duckdb')
# Register Parquet files as external tables
years = ['2022', '2023', '2024']
for year in years:
file_path = f'data/historical_jobs_{year}.parquet'
conn.execute(f"CREATE OR REPLACE VIEW jobs_{year} AS SELECT * FROM '{file_path}'")
# Create unified view
conn.execute("""
CREATE OR REPLACE VIEW all_jobs AS
SELECT * FROM jobs_2024
UNION ALL SELECT * FROM jobs_2023
UNION ALL SELECT * FROM jobs_2022
""")
# Query salary trends by year
salary_trends = conn.execute("""
SELECT
EXTRACT(year FROM positionOpenDate) as year,
ROUND(AVG(minimumSalary)) as avg_min,
ROUND(AVG(maximumSalary)) as avg_max,
ROUND(percentile_cont(0.5) WITHIN GROUP (ORDER BY minimumSalary)) as median_min,
ROUND(percentile_cont(0.5) WITHIN GROUP (ORDER BY maximumSalary)) as median_max,
COUNT(*) as jobs
FROM all_jobs
WHERE minimumSalary IS NOT NULL
GROUP BY EXTRACT(year FROM positionOpenDate)
ORDER BY year DESC
""").fetchdf()
print(salary_trends)
# Monthly posting patterns
monthly_posts = df_2024.groupby(df_2024['positionOpenDate'].dt.month).size()
print("Monthly posting patterns:")
month_names = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']
for month, count in monthly_posts.items():
percentage = count / len(df_2024) * 100
print(f" {month_names[month-1]}: {count:,} jobs ({percentage:.1f}%)")
# Most common position titles
common_titles = df_2024['positionTitle'].value_counts().head(10)
print("Most common position titles:")
for i, (title, count) in enumerate(common_titles.items(), 1):
percentage = count / len(df_2024) * 100
print(f" {i:2d}. {title}: {count:,} ({percentage:.1f}%)")