This report visualises 2019 Detroit crime incidents
drawn from the Open Crime Database
via the crimedata R package. Five high-impact offense
categories are mapped interactively using leaflet:
Note: Up to 8,000 incidents are sampled for browser performance. Toggle between Points by Offense and Heat Map layers using the control panel on the map.
library(crimedata)
library(leaflet)
library(leaflet.extras)
library(dplyr)
library(RColorBrewer)
library(DT)
library(scales)crimes_raw <- get_crime_data(
years = 2019,
cities = "Detroit",
type = "core"
)
cat(sprintf("Raw records loaded: %s\n", format(nrow(crimes_raw), big.mark = ",")))## Raw records loaded: 82,682
## Columns available: uid, city_name, offense_code, offense_type, offense_group, offense_against, date_single, longitude, latitude, census_block
target_offenses <- c(
"assault offenses",
"burglary/breaking & entering",
"motor vehicle theft",
"robbery",
"homicide offenses"
)
crimes <- crimes_raw |>
filter(
!is.na(longitude),
!is.na(latitude),
offense_group %in% target_offenses
) |>
mutate(
offense_group = as.character(offense_group),
offense_label = paste0(
toupper(substring(offense_group, 1, 1)),
substring(offense_group, 2)
),
date_fmt = format(date_single, "%b %d, %Y")
) |>
slice_sample(n = min(8000, nrow(crimes_raw))) %>%
mutate(
offense_label = tools::toTitleCase(offense_group),
date_fmt = format(date_single, "%b %d, %Y")
)
cat(sprintf("Incidents after filtering & sampling: %s\n", format(nrow(crimes), big.mark = ",")))## Incidents after filtering & sampling: 8,000
## Offense categories: 5
summary_tbl <- crimes %>%
count(offense_label, name = "Count") %>%
arrange(desc(Count)) %>%
mutate(
Percent = paste0(round(Count / sum(Count) * 100, 1), "%")
) %>%
rename(`Offense Type` = offense_label)
datatable(
summary_tbl,
options = list(pageLength = 10, dom = "t"),
rownames = FALSE,
caption = "Table 1 — Sampled incidents by offense category (Detroit, 2019)"
)monthly <- crimes %>%
mutate(month = format(date_single, "%b"), month_n = as.integer(format(date_single, "%m"))) %>%
count(month, month_n, offense_label) %>%
arrange(month_n)
# Base R barplot for zero extra dependencies
monthly_wide <- tapply(monthly$n, list(monthly$month, monthly$offense_label), sum)
monthly_wide[is.na(monthly_wide)] <- 0
month_order <- month.abb[month.abb %in% rownames(monthly_wide)]
monthly_wide <- monthly_wide[month_order, , drop = FALSE]
par(bg = "#222", fg = "#eee", col.axis = "#eee", col.lab = "#eee",
col.main = "#fff", mar = c(4, 4, 3, 1))
barplot(
t(monthly_wide),
beside = TRUE,
col = brewer.pal(ncol(monthly_wide), "Set1"),
names.arg = rownames(monthly_wide),
legend.text = colnames(monthly_wide),
args.legend = list(x = "topright", bty = "n", cex = 0.75, text.col = "#eee"),
main = "Monthly Crime Incidents by Offense Type — Detroit 2019",
xlab = "Month",
ylab = "Incident Count",
border = NA,
las = 1
)categories <- sort(unique(crimes$offense_label))
palette_cols <- brewer.pal(max(3, length(categories)), "Set1")[seq_along(categories)]
pal <- colorFactor(palette = palette_cols, domain = categories)
map <- leaflet(crimes) %>%
# --- Basemap tiles ---
addProviderTiles(providers$CartoDB.DarkMatter, group = "Dark") %>%
addProviderTiles(providers$CartoDB.Positron, group = "Light") %>%
addProviderTiles(providers$Esri.WorldImagery, group = "Satellite") %>%
# --- Heatmap overlay ---
addHeatmap(
lng = ~longitude,
lat = ~latitude,
intensity = 1,
blur = 18,
max = 0.05,
radius = 12,
group = "Heat Map"
) %>%
# --- Circle markers coloured by offense type ---
addCircleMarkers(
lng = ~longitude,
lat = ~latitude,
color = ~pal(offense_label),
radius = 4,
stroke = FALSE,
fillOpacity = 0.65,
popup = ~paste0(
"<b style='font-size:13px;'>", offense_label, "</b><br>",
"<i>", date_fmt, "</i><br>",
"<span style='color:#888;'>Census Block: ", census_block, "</span>"
),
group = "Points by Offense"
) %>%
# --- Legend ---
addLegend(
position = "bottomright",
pal = pal,
values = ~offense_label,
title = "Offense Type",
opacity = 0.85
) %>%
# --- Layer switcher ---
addLayersControl(
baseGroups = c("Dark", "Light", "Satellite"),
overlayGroups = c("Points by Offense", "Heat Map"),
options = layersControlOptions(collapsed = FALSE)
) %>%
hideGroup("Heat Map") %>%
# --- UI extras ---
addMiniMap(toggleDisplay = TRUE, minimized = TRUE) %>%
addScaleBar(position = "bottomleft") %>%
addResetMapButton() %>%
# --- Title overlay ---
addControl(
html = "<div style='
background:rgba(0,0,0,0.7);
color:#fff;
padding:8px 14px;
border-radius:6px;
font-family:Georgia,serif;
font-size:14px;
line-height:1.5;'>
<b>Detroit Crime Map — 2019</b><br>
<span style='font-size:11px;color:#bbb;'>
Click any marker for offense details
</span>
</div>",
position = "topleft"
)
mapThe table below shows a random sample of 200 records from the filtered dataset. Use the search box to filter by offense type, census_block, or date.
crimes %>%
select(
Date = date_fmt,
`Offense` = offense_label,
census_block = census_block,
Latitude = latitude,
Longitude = longitude
) %>%
slice_sample(n = 200) %>%
datatable(
filter = "top",
options = list(pageLength = 10, scrollX = TRUE),
rownames = FALSE,
caption = "Table 2 — Sample of 200 incidents (randomly drawn)"
)| Parameter | Current Value | How to Change |
|---|---|---|
| City | Detroit | Replace in get_crime_data(cities = ...) |
| Year | 2019 | Replace in get_crime_data(years = ...) |
| Sample size | 8,000 | Change n = min(8000, n()) |
| Offense filter | 5 categories | Edit target_offenses vector |
| Basemap default | Dark | Swap addProviderTiles order |
Export tip: To save the map as a standalone HTML file, add this chunk at the end:
Report generated with R 4.5.2 · crimedata ·
leaflet · leaflet.extras ·
DT