Going back to when I was at school, one assembly a Police officer came in to give a presentation. The presentation consisted of various interesting data points related to my town of Kings Lynn. The number of crimes that happen daily, the number of Police call-outs and — most interestingly to me — the percentage of crimes based on area.
Today — for reasons, not even my fickle brain can explain — I was transported back to that presentation. I stumbled across the Police Data API and set out to prove visually, what that Police officer was telling us, all those years ago.
data.police.uk
data.police.uk is a site for open data about crime and policing in England, Wales, and Northern Ireland.
The API provides a rich data source for information:
- Neighbourhood team members
- Upcoming events
- Street-level crime and outcome data
- Nearest police stations
I live in Norfolk, so to get information about my local police force I can call the API like so:
GET https://data.police.uk/api/forces/norfolk
Response:
{
"description": null,
"url": "https://www.norfolk.police.uk",
"engagement_methods": [
{
"url": "https://www.facebook.com/NorfolkPolice",
"type": "facebook",
"description": null,
"title": "facebook"
},
{
"url": "https://twitter.com/NorfolkPolice",
"type": "twitter",
"description": null,
"title": "twitter"
}
],
"telephone": "101",
"id": "norfolk",
"name": "Norfolk Constabulary"
}
But that’s not quite what I’m interested in. I want to know where the crimes are happening. The Street-level crimes endpoint is more like it.
Param | Description |
---|---|
lat | Latitude of the requested crime area |
lng | Longitude of the requested crime area |
date | Optional. (YYYY-MM) Limit results to a specific month. The latest month will be shown by default |
So providing a latitude and a longitude it’ll return the crimes within a 1-mile radius. Here’s an example request:
GET https://data.police.uk/api/crimes-street/all-crime?lat=52.7517&lng=0.4023&date=2021-12
Response:
[
{
"category": "anti-social-behaviour",
"location_type": "Force",
"location": {
"latitude": "52.740958",
"street": {
"id": 999838,
"name": "On or near Hardwick Road"
},
"longitude": "0.408674"
},
"context": "",
"outcome_status": null,
"persistent_id": "",
"id": 97924131,
"location_subtype": "",
"month": "2021-12"
},
...
Tracking crimes
This one endpoint should give me enough data to make something visual.
Google maps
Using google cloud I set up a google maps service so I can use it in my application. It’s a simple process, you just need to configure some credentials to generate an API key.
Then it’s a case of including the script tag:
<script src="https://maps.googleapis.com/maps/api/js?key=API_KEY&callback=initMap&v=weekly&channel=2"async></script>
And setting up a callback function:
async function initMap() {
console.log('initing map');
map = new google.maps.Map(document.getElementById("map"), {
center: { lat: 52.7517, lng: 0.4023 },
zoom: 12,
});
Which gives you the map:

Getting the data points
Because the API returns data for a set radius I need to set some points on the map that’ll be leveraged to call the API, I created a simple function to do this:
function createLongsAndLats() {
let count = 5;
let startLat = 52.717;
let lat = startLat;
let lng = 0.2323;
for (let i = 0; i < count; i++) {
for (let j = 0; j < count; j++) {
if (showAreas) {
const marker = new google.maps.Marker({
position: { lat: lat, lng: lng },
map: map
});
}
positions.push({
lat: lat,
lng: lng
});
lat += latDifference;
}
lng += longDifference;
lat = startLat;
}
}
The showAreas
flag is purely for this blog. If enabled the map looks like:

So for a given month, these points will be sent up to the API to collect data about the crimes in the area.
async function getCrimesForMonth(year, month) {
let crimes = [];
for (let i = 0; i < positions.length; i++) {
let areaCrime = await httpGet(`https://data.police.uk/api/crimes-street/all-crime?lat=${positions[i].lat}&lng=${positions[i].lng}&date=${year}-0${month}`);
if (areaCrime) {
for (let crime of areaCrime) {
crimes.push(crime);
}
}
}
crimeMap.set(`${year}-${month}`, crimes);
}
The crimes are then added to a map. Due to limitations with the API, throttling at 15 requests per second I’ll have to continuously be collecting the data as the application renders. I could theoretically collect all the data locally from a one time run, but that would be too easy.
I’m then displaying the crimes based on their exact longitude and latitude returned in the response.
async function drawCrimes(year, month) {
const allCrimes = crimeMap.get(`${year}-${month}`);
for (let crime of allCrimes) {
await delay(300);
const marker = new google.maps.Marker({
position: { lat: parseFloat(crime.location.latitude), lng: parseFloat(crime.location.longitude)},
map: map
});
markers.push(marker);
}
deleteMarkers();
}
Which looks something like this:

It then iterates through each month, resetting the markers.
It would be interesting if I could then go and click on a marker and learn what the crime was, let’s add that in.
Not only do I want to know what the crime was, but it’d be cool to see at a glance the outcome of the crime. To get the full outcome object there’s another endpoint for that, so a separate request is needed:
GET https://data.police.uk/api/outcomes-for-crime/<persistent_id>
Response:
{
"outcomes": [
{
"category": {
"code": "under-investigation",
"name": "Under investigation"
},
"date": "2021-12",
"person_id": null
},
{
"category": {
"code": "unable-to-prosecute",
"name": "Unable to prosecute suspect"
},
"date": "2021-12",
"person_id": null
},
{
"category": {
"code": "unable-to-prosecute",
"name": "Unable to prosecute suspect"
},
"date": "2021-12",
"person_id": null
}
],
"crime": {
"category": "criminal-damage-arson",
"location_type": "Force",
"location": {
"latitude": "52.755118",
"street": {
"id": 1000242,
"name": "On or near Tennyson Avenue"
},
"longitude": "0.410967"
},
"context": "",
"persistent_id": "4b54eabcee82f608df4cd02311e5cdb4a4c304f277c5d705ec988fe86d57eed8",
"id": 97917521,
"location_subtype": "",
"month": "2021-12"
}
}
Let’s collate all of this into a marker:
const contentString =
'<div id="content">' +
'<div id="siteNotice">' +
"</div>" +
`<h1 id="firstHeading" class="firstHeading">${crime.category}</h1>` +
'<div id="bodyContent">' +
`<p>${crime.location.street.name}</p>` +
`<p>${await getCrimeOutcome(crime.persistent_id)}</p>` +
`(last updated ${crime.month}).</p>` +
"</div>" +
"</div>";
const infowindow = new google.maps.InfoWindow({
content: contentString,
});
marker.addListener("click", () => {
infowindow.open({ anchor: marker, map, shouldFocus: false });
});
So now I can dive into the crimes on the map:

Conclusion
Well, the Police officer was right. Many years later and the crimes are still happening predominantly in the same areas. It’s interesting how the crimes are so predictable, certain areas continuously getting on average 4 crimes per month, and others at least 30. The locations change somewhat with the changing of the seasons, more crimes around lakes and parks when the weather gets warmer.
There’s still a lot more that can be done with this: It’d be intriguing to integrate some machine learning and attempt to predict upcoming crimes. Or make it possible to create a selection of areas you want to monitor, which might come in handy if I ever decide to move to a different city, wherever the crime’s happening might be a place not to buy a house. That’s plenty for another blog. Let me know if you have any ideas!
If you liked this blog then please sign up for my newsletter and join an awesome community!
Very interesting!