The improved code with help from Microsoft Copilot helped to make a slight, but big improvement to the map on display in this document. My first map had a problem due to the fact that many ArcGIS services limit their records returned per request. What this means is that if the number of incidents exceeds the server limit, it will not be included in the data. To fix this, Copilot recommended to add pagination, which would help to get the data included. What it has done is given the code the ability to page through records until fewer of them than the total page size are left. This means that the data is now all-encompassing and creates a clearer picture with more overall accuracy. Obviously, this is critical when trying to measure total incidents in an area. This updated map is demonstrated below.

I used Copilot to find ideas for improvements that could be made to the Frequency Map, and there were a few interesting ideas including the ArcGIS improvement. Another good improvement could have been using a better frequency calculation, which would have helped to better break down the specifics of the cases, and given us a more detailed look at where cases fall in relation to each other. It would also have given us a better look at which areas are at the top when it comes to number of incidents without too much of a generalization. One suggestion that I did not use had to do with download size, which is more of a quality of life improvement than anything else. While it is nice to have, it was not necessary for getting the same result. It would not have had nearly the same impact as the other suggestions.

Code

Here is the code that was used to come up with the new updated map as well as more accurate data.

############################################################
# Lesson 1: Mapping Crime Incidents with Points
#
# This script creates a frequency-class map showing where
# aggravated assaults occurred during Summer 2026.
#
# Larger circles represent locations with more incidents.
#
# Circle colors represent frequency categories:
# Gray = One Incident
# Gold = Two to Three Incidents
# Red = Four or More Incidents
############################################################

############################################################
# Step 1: Install Required Packages
############################################################

required_packages <- c(
  "jsonlite",
  "tidyverse",
  "leaflet"
)

missing_packages <- required_packages[
  !sapply(required_packages, requireNamespace, quietly = TRUE)
]

if (length(missing_packages) > 0) {
  install.packages(missing_packages)
}

############################################################
# Step 2: Load Required Packages
############################################################

library(jsonlite)
library(tidyverse)
library(leaflet)

############################################################
# Step 3: Download Aggravated Assault Data
#
# Retrieve Summer 2026 aggravated assault incidents
# (NIBRS code 13A) from Nashville's crime API.
#
# Uses ArcGIS pagination to ensure all records are
# downloaded, even when the service limits the
# number returned per request.
############################################################

base_url <- paste0(
  "https://services2.arcgis.com/HdTo6HJqh92wn4D8/",
  "arcgis/rest/services/",
  "Metro_Nashville_Police_Department_Incidents_view/",
  "FeatureServer/0/query"
)

query <- paste(
  "Incident_Occurred >= DATE '2026-06-01'",
  "AND Incident_Occurred < DATE '2026-09-01'",
  "AND Offense_NIBRS = '13A'"
)

############################################################
# Determine ArcGIS Record Limit
############################################################

service_info <- tryCatch(
  {
    fromJSON(
      paste0(base_url, "?f=json")
    )
  },
  error = function(e) {
    stop(
      "Unable to access ArcGIS service metadata:\n",
      e$message
    )
  }
)

page_size <- ifelse(
  !is.null(service_info$maxRecordCount),
  service_info$maxRecordCount,
  2000
)

cat(
  "ArcGIS page size:",
  page_size,
  "records per request\n"
)

############################################################
# Download All Pages of Data
############################################################

offset <- 0
all_data <- list()

repeat {
  
  crime_url <- paste0(
    base_url,
    "?where=",
    URLencode(query, reserved = TRUE),
    "&outFields=*",
    "&f=json",
    "&resultOffset=",
    offset,
    "&resultRecordCount=",
    page_size
  )
  
  response <- tryCatch(
    {
      fromJSON(crime_url)
    },
    error = function(e) {
      stop(
        "Failed while downloading crime data:\n",
        e$message
      )
    }
  )
  
  if (
    is.null(response$features) ||
    length(response$features) == 0
  ) {
    break
  }
  
  batch <- response$features$attributes
  
  all_data[[length(all_data) + 1]] <- batch
  
  cat(
    "Downloaded",
    nrow(batch),
    "records (offset =",
    offset,
    ")\n"
  )
  
  if (nrow(batch) < page_size) {
    break
  }
  
  offset <- offset + page_size
}

CrimeData <- bind_rows(all_data)

############################################################
# Verify Records Were Returned
############################################################

if (nrow(CrimeData) == 0) {
  stop(
    "No aggravated assault incidents were returned."
  )
}

cat(
  "\nTotal aggravated assault incidents downloaded:",
  nrow(CrimeData),
  "\n"
)

############################################################
# Step 4: Prepare the Data
#
# Convert date fields and remove records that are
# missing latitude or longitude coordinates.
############################################################

CrimeData <- CrimeData |>
  mutate(
    Incident_Occurred = as.POSIXct(
      Incident_Occurred / 1000,
      origin = "1970-01-01",
      tz = "America/Chicago"
    ),
    Incident_Reported = as.POSIXct(
      Incident_Reported / 1000,
      origin = "1970-01-01",
      tz = "America/Chicago"
    )
  ) |>
  filter(
    !is.na(Longitude),
    !is.na(Latitude)
  )

cat(
  "Number of geocoded aggravated assault incidents:",
  nrow(CrimeData),
  "\n"
)

############################################################
# Step 5: Aggregate Incidents by Location
#
# Round coordinates slightly to prevent duplicate
# locations caused by tiny GPS differences.
############################################################

CrimeLocations <- CrimeData |>
  mutate(
    Latitude = round(Latitude, 5),
    Longitude = round(Longitude, 5)
  ) |>
  count(
    Latitude,
    Longitude,
    sort = TRUE,
    name = "Incidents"
  )

############################################################
# Step 6: Create Frequency Categories
############################################################

CrimeLocations <- CrimeLocations |>
  mutate(
    FrequencyGroup = case_when(
      Incidents == 1 ~ "One Incident",
      Incidents <= 3 ~ "Two to Three Incidents",
      TRUE ~ "Four or More Incidents"
    )
  )

############################################################
# Step 7: Create Popup Content
############################################################

LocationPopup <- ~paste0(
  "<strong>Incidents at This Location:</strong> ",
  Incidents,
  
  "<br><strong>Frequency Category:</strong> ",
  FrequencyGroup,
  
  "<br><br><strong>Coordinates:</strong><br>",
  round(Latitude, 6),
  ", ",
  round(Longitude, 6)
)

############################################################
# Step 8: Assign Colors to Categories
############################################################

CrimeLocations <- CrimeLocations |>
  mutate(
    DotColor = case_when(
      FrequencyGroup == "One Incident" ~ "gray",
      FrequencyGroup == "Two to Three Incidents" ~ "gold",
      FrequencyGroup == "Four or More Incidents" ~ "red"
    )
  )

############################################################
# Step 9: Create the Frequency-Class Map
############################################################

FrequencyClassMap <- leaflet(CrimeLocations) |>
  addProviderTiles(
    "Esri.WorldStreetMap"
  ) |>
  
  addCircleMarkers(
    lng = ~Longitude,
    lat = ~Latitude,
    radius = ~sqrt(Incidents) * 3,
    stroke = TRUE,
    weight = 1,
    color = "black",
    fillColor = ~DotColor,
    fillOpacity = 0.7,
    popup = LocationPopup
  ) |>
  
  addLegend(
    position = "bottomright",
    colors = c(
      "gray",
      "gold",
      "red"
    ),
    labels = c(
      "One Incident",
      "Two to Three Incidents",
      "Four or More Incidents"
    ),
    title = "Incident Frequency"
  ) |>
  
  addControl(
    html = paste(
      "<b>Summer 2026 Aggravated Assaults</b><br>",
      "Nashville, Tennessee"
    ),
    position = "topright"
  )

############################################################
# Display Map
############################################################

FrequencyClassMap