Overview

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:

  • Assault Offenses
  • Burglary / Breaking & Entering
  • Motor Vehicle Theft
  • Robbery
  • Homicide Offenses

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.


1. Install & Load Packages

library(crimedata)
library(leaflet)
library(leaflet.extras)
library(dplyr)
library(RColorBrewer)
library(DT)
library(scales)

2. Load Crime Data

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
cat(sprintf("Columns available: %s\n", paste(names(crimes_raw), collapse = ", ")))
## Columns available: uid, city_name, offense_code, offense_type, offense_group, offense_against, date_single, longitude, latitude, census_block

3. Clean & Filter

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
cat(sprintf("Offense categories: %d\n", n_distinct(crimes$offense_label)))
## Offense categories: 5

4. Summary Statistics

4.1 Incidents by Offense Type

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)"
)

4.2 Monthly Distribution

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
)


5. Interactive Crime Map

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"
  )

map

6. Raw Data Preview

The 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)"
  )

7. Notes & Customisation

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
htmlwidgets::saveWidget(map, "Detroit_crime_map_2019.html", selfcontained = TRUE)

Export tip: To save the map as a standalone HTML file, add this chunk at the end:

htmlwidgets::saveWidget(map, "Detroit_crime_map_2019.html", selfcontained = TRUE)

Report generated with R 4.5.2 · crimedata · leaflet · leaflet.extras · DT