The fbiCDE package
wraps the FBI’s Crime Data Explorer (CDE) API (https://cde.ucr.cjis.gov/LATEST/), giving R users direct
access to Uniform Crime Reporting (UCR) and National Incident-Based
Reporting System (NIBRS) data: crime counts and rates, arrests, NIBRS
victim/offender/offense detail, Supplemental Homicide Reports (SHR),
police employment, NIBRS participation, and officers killed (LEOKA).
No API key is required. (get_api_key() and
set_api_key() remain only for compatibility with the
package’s api.data.gov-hosted predecessor and do
nothing.)
The CDE itself knows three geographies: the nation, a state, and a
single agency. This vignette covers those. The package also builds
county, city and metro-area views on top of them; see
vignette("geography").
Everything below was run against the live API on 2026-10-06. Agencies backfill and revise their reports, so re-running it will move some numbers.
Before requesting crime data you often need a state abbreviation, an offense code, or an agency’s ORI (the 9-character identifier the CDE uses for law enforcement agencies).
states <- get_states()
head(states)
#> stateAbbreviation stateName
#> 1 AL Alabama
#> 2 AK Alaska
#> 3 AZ Arizona
#> 4 AR Arkansas
#> 5 CA California
#> 6 CO Coloradoget_agency_info() looks agencies up by name in the
package’s bundled agency table (fbi_api_agencies), with no
network call. Note that state here is the full state
name:
get_agency_info("Oakland Police Department", state = "california", ori_only = TRUE)
#> agency_name ori
#> 905 Oakland Police Department CA0010900The agencies used below:
| ORI | Agency | Used for |
|---|---|---|
CA0010900 |
Oakland Police Department, CA | crime, police employment |
OHCOP0000 |
Columbus Police Department, OH | arrests, participation |
GAAPD0000 |
Atlanta Police Department, GA | homicide reports |
get_agency_crime() returns an agency’s monthly offenses
and clearances; get_reported_crime() does the same for a
state, or the nation when state_abb is NULL;
get_reported_arson() is get_reported_crime()
for arson.
The offense argument takes a code. The default
"V" is violent crime and "P" property crime;
get_offense_codes() lists the rest:
offenses <- get_offense_codes("crime-trend")
head(offenses, 8)
#> code label
#> 1 ASS Aggravated Assault
#> 2 HOM Homicide
#> 3 RPE Rape
#> 4 ROB Robbery
#> 5 ARS Arson
#> 6 BUR Burglary
#> 7 LAR Larceny-theft
#> 8 MVT Motor Vehicle Theftoakland_crime <- get_agency_crime("CA0010900", from = "01-2019", to = "03-2019")
oakland_crime
#> geography offense measure period count rate population
#> 1 CA0010900 V offenses 01-2019 472 108.75 434036
#> 2 CA0010900 V offenses 02-2019 368 84.79 434036
#> 3 CA0010900 V offenses 03-2019 438 100.91 434036
#> 4 CA0010900 V clearances 01-2019 106 24.42 434036
#> 5 CA0010900 V clearances 02-2019 40 9.22 434036
#> 6 CA0010900 V clearances 03-2019 37 8.52 434036
#> participated_population
#> 1 434036
#> 2 434036
#> 3 434036
#> 4 434036
#> 5 434036
#> 6 434036Each row is one month of one measure (offenses or
clearances). rate is per 100,000 people for that month.
population is the population the agency serves and
participated_population the part of it covered by a report
that month; for a single agency they match whenever it reported.
For an agency or a state, the API also sends its state’s and the
nation’s rates for comparison. Those are dropped by default;
comparison = TRUE keeps them, labelled in
series and series_name (they have no
count):
with_context <- get_agency_crime("CA0010900", from = "01-2019", to = "01-2019",
comparison = TRUE)
with_context[, c("series", "series_name", "measure", "count", "rate")]
#> series series_name measure count rate
#> 1 agency Oakland Police Department offenses 472 108.75
#> 2 agency Oakland Police Department clearances 106 24.42
#> 3 state California offenses NA 34.38
#> 4 state California clearances NA 16.12
#> 5 national United States offenses NA 27.85
#> 6 national United States clearances NA 12.20texas_crime <- get_reported_crime("TX", from = "01-2019", to = "03-2019")
texas_crime
#> geography offense measure period count rate population
#> 1 TX V offenses 01-2019 9690 33.64 28995881
#> 2 TX V offenses 02-2019 8554 29.69 28995881
#> 3 TX V offenses 03-2019 9768 33.89 28995881
#> 4 TX V clearances 01-2019 3426 11.89 28995881
#> 5 TX V clearances 02-2019 3083 10.70 28995881
#> 6 TX V clearances 03-2019 3266 11.33 28995881
#> participated_population
#> 1 28804627
#> 2 28815089
#> 3 28824219
#> 4 28804627
#> 5 28815089
#> 6 28824219At state and national level,
participated_population / population is the reporting
coverage: the count is the sum of the agencies that reported, not an
estimate for everyone.
national_arson <- get_reported_arson(from = "01-2019", to = "03-2019")
national_arson[, c("geography", "measure", "period", "count", "rate")]
#> geography measure period count rate
#> 1 US offenses 01-2019 2598 0.81
#> 2 US offenses 02-2019 2218 0.69
#> 3 US offenses 03-2019 2771 0.86
#> 4 US clearances 01-2019 572 0.18
#> 5 US clearances 02-2019 487 0.15
#> 6 US clearances 03-2019 684 0.21get_arrest_count() with the default
offense = "all" returns monthly arrest totals:
columbus_arrests <- get_arrest_count("OHCOP0000", from = "01-2023", to = "03-2023")
columbus_arrests
#> geography offense measure period count rate population
#> 1 OHCOP0000 all arrests 01-2023 617 67.89 908834
#> 2 OHCOP0000 all arrests 02-2023 606 66.68 908834
#> 3 OHCOP0000 all arrests 03-2023 674 74.16 908834
#> participated_population
#> 1 908834
#> 2 908834
#> 3 908834Naming an offense returns that offense’s monthly series. Names come
at three levels of detail (list_ucr_arrest_offenses() lists
them); within one level the counts do not overlap. The API addresses an
offense by a numeric code, one per breakdown
(ucr_arrest_offense_codes), so a name spanning several
codes costs one request per code and the series is their sum:
head(list_ucr_arrest_offenses(level = "category"))
#> [1] "Aggravated Assault"
#> [2] "All Other Offenses"
#> [3] "Arson"
#> [4] "Burglary"
#> [5] "Counterfeiting/Forgery"
#> [6] "Destruction/Damage/Vandalism of Property"
drugs <- get_arrest_count(state_abb = "OH", offense = "Drug/Narcotic Offenses",
from = "01-2023", to = "03-2023")
drugs[, c("offense", "period", "count", "rate")]
#> offense period count rate
#> 1 Drug/Narcotic Offenses 01-2023 1897 16.96
#> 2 Drug/Narcotic Offenses 02-2023 1884 16.93
#> 3 Drug/Narcotic Offenses 03-2023 2174 19.46One name to watch: "Drug Abuse Violations" is only the
drug arrests not classed as possession or sale. The category
"Drug/Narcotic Offenses" above is the total.
get_arrest_demographics() returns arrestees by sex, race
and age by sex, summed over the date range, for all offenses or one. For
one offense, that is the age-by-offense cross-tabulation:
larceny <- get_arrest_demographics(state_abb = "OH", offense = "Larceny",
from = "01-2023", to = "12-2023")
larceny[larceny$demographic_type == "Arrestee Sex", ]
#> geography offense period demographic_type demographic_value count
#> 1 OH Larceny <NA> Arrestee Sex Male 10323
#> 2 OH Larceny <NA> Arrestee Sex Female 9177vignette("juvenile-arrests") works through these at
state, agency and metro level.
get_nibrs_victim(), get_nibrs_offender()
and get_nibrs_offense() return one NIBRS breakdown for one
offense, summed over the date range. The offense must be a code: a
summary group such as "V", "ROB" or
"BUR", or a NIBRS offense code such as "13B"
(simple assault):
head(list_nibrs_offenses(), 10)
#> code label
#> 1 V Violent Crime
#> 2 P Property Crime
#> 3 ASS Aggravated Assault
#> 4 HOM Homicide
#> 5 RPE Rape
#> 6 ROB Robbery
#> 7 ARS Arson
#> 8 BUR Burglary
#> 9 LAR Larceny-theft
#> 10 MVT Motor Vehicle Theftget_nibrs_victim(state_abb = "OH", offense = "ROB", variable = "age",
from = "01-2023", to = "12-2023")
#> geography offense period demographic_type demographic_value count
#> 1 OH ROB age 0-9 29
#> 2 OH ROB age 10-19 847
#> 3 OH ROB age 20-29 1711
#> 4 OH ROB age 30-39 1421
#> 5 OH ROB age 40-49 932
#> 6 OH ROB age 50-59 665
#> 7 OH ROB age 60-69 455
#> 8 OH ROB age 70-79 135
#> 9 OH ROB age 80-89 26
#> 10 OH ROB age Unknown 25
#> 11 OH ROB age 90-Older 9The variables differ by function:
list_nibrs_victim_variables(),
list_nibrs_offender_variables() and
list_nibrs_offense_variables() list them.
get_nibrs_offense(state_abb = "OH", offense = "ROB", variable = "weapons",
from = "01-2023", to = "12-2023")[, c("demographic_value", "count")]
#> demographic_value count
#> 1 None 2615
#> 2 Other 116
#> 3 Rifle 24
#> 4 Poison 1
#> 5 Firearm 474
#> 6 Handgun 1332
#> 7 Shotgun 9
#> 8 Unknown 221
#> 9 Explosives 2
#> 10 Asphyxiation 3
#> 11 Blunt Object 68
#> 12 Other Firearm 24
#> 13 Personal Weapons 928
#> 14 Rifle (Automatic) 1
#> 15 Firearm (Automatic) 0
#> 16 Handgun (Automatic) 31
#> 17 Shotgun (Automatic) 0
#> 18 Motor Vehicle/Vessel 28
#> 19 Fire/Incendiary Device 2
#> 20 Knife/Cutting Instrument 239
#> 21 Other Firearm (Automatic) 0
#> 22 Drugs/Narcotics/Sleeping Pills 2An offense name returns no data from the API, so the package rejects it and suggests the code:
get_nibrs_victim(state_abb = "OH", offense = "robbery")
#> Error: 'robbery' is not a NIBRS offense code; the API returns no data for offense names. Did you mean "ROB" (Robbery)? See list_nibrs_offenses().An agency or state that did not report to NIBRS in the period returns no rows, with a message.
get_shr() returns monthly homicide counts from the SHR,
for an agency, a state, or the nation:
get_shr(ori = "GAAPD0000", from = "01-2019", to = "06-2019")
#> geography period count
#> 1 GAAPD0000 01-2019 8
#> 2 GAAPD0000 02-2019 9
#> 3 GAAPD0000 03-2019 9
#> 4 GAAPD0000 04-2019 7
#> 5 GAAPD0000 05-2019 7
#> 6 GAAPD0000 06-2019 7A zero can mean “not reported”. Florida, for one, sends almost no SHR data:
get_police_employment() returns sworn officers and
civilian staff by sex, for an agency, a state or the nation. It uses
four-digit years rather than MM-YYYY:
oakland_pe <- get_police_employment("CA0010900", from = "2018", to = "2020")
oakland_pe
#> year male_officers female_officers male_civilians female_civilians ori
#> 1 2018 633 98 60 219 CA0010900
#> 2 2019 636 104 63 220 CA0010900
#> 3 2020 632 108 83 233 CA0010900
#> male_total female_total civilians_total officers_total employees_total
#> 1 693 317 279 731 1010
#> 2 699 324 283 740 1023
#> 3 715 341 316 740 1056
#> participated_population employees_per_1000
#> 1 430230 2.35
#> 2 434036 2.36
#> 3 437923 2.41A state’s or the nation’s counts are sums over the agencies that reported that year, and that set changes, so compare years by the rate rather than the count:
texas_pe <- get_police_employment(state_abb = "TX", from = "2018", to = "2020")
texas_pe[, c("year", "employees_total", "participated_population",
"employees_per_1000")]
#> year employees_total participated_population employees_per_1000
#> 1 2018 70186 20609733 3.40
#> 2 2019 86760 25744202 3.36
#> 3 2020 97865 28635940 3.41Texas’s employee count rose 39% from 2018 to 2020 because more of the state reported; per 1,000 people covered it barely moved.
get_agency_participation(),
get_state_participation() and
get_region_participation() report whether agencies report
to NIBRS now, from the live agency directory. They are a
current snapshot, not a history, and they count agencies, not people: a
state with few NIBRS agencies can still have near-complete reporting
coverage through the older summary system (see the
participated_population columns above).
get_agency_participation("OHCOP0000")
#> ori agency_name agency_type_name state_abbr state_name
#> 1 OHCOP0000 Columbus Police Department City OH Ohio
#> county is_nibrs nibrs_start_date
#> 1 DELAWARE, FAIRFIELD, FRANKLIN TRUE 2003-01-01get_leoka() and get_leoka_monthly() return
national counts of law enforcement officers feloniously
killed, from 2020 on. Despite the program’s name (Law
Enforcement Officers Killed and Assaulted) these are not assault counts.
The API returns national figures only.