Querying the Police UK API – Lincoln, UK Crime Rates

38 minutes ago 1

It's always been in the back of my mind - When you hear about crimes going unsolved in your local community and indeed nationwide in various news feeds. Anecdotally I've heard friends/colleagues with their various stories regarding some crime and the police either didn't attend or had a piss poor attempt. This can often amount to nobody coming out to investigate and giving a crime number so the insurance company can tick their boxes and everyone moves on with their life.

In the previous years I've dabbled with programming and python for some personal projects. They're extremely rough projects with poorly written code, however I usually don't care as long as it works. I came across the police API when searching for some information about crimes (a serious crime happened around my corner and was curious how it was logged). This project is me just trying to learn, some AI was used where I got stuck with iterating through the dates (func def month_range and the final lambda).

This then got me thinking what data can I get and how can I present it to

Link to the police API documentation: https://data.police.uk/docs/


I was most interested in burglaries and after checking the API it looks like street level crimes allowed me to pass in a custom date range and custom location.

Example query from the docs:

https://data.police.uk/api/crimes-street/all-crime?date=2024-01&poly=52.268,0.543:52.794,0.238:52.130,0.478

When running a query and outputting a single output it gives:

{'category': 'violent-crime', 'location_type': 'Force', 'location': {'latitude': '52.373536', 'street': {'id': 1609636, 'name': 'On or near Cutters Close'}, 'longitude': '0.480144'}, 'context': '', 'outcome_status': {'category': 'Unable to prosecute suspect', 'date': '2024-02'}, 'persistent_id': 'f997b1bd08ebb4bee0156b757d93ae2e67ac90225b0b5f3db0231286d4001372', 'id': 115990092, 'location_subtype': '', 'month': '2024-01'}

That's then given me:

  • date
  • Location
  • Category
  • Outcome of the crime.

So it should be really easy to parse this into some tables or graphs.

Location

I will get the GPS co-ordinates for the location I'm interested in. In this case Lincoln, UK They can then be passed in as a poly set of GPS coordinates:

The poly parameter is formatted in lat/lng pairs, separated by colons:
[lat],[lng]:[lat],[lng]:[lat],[lng]
The first and last coordinates need not be the same — they will be joined by a straight line once the request is made.


Shape the coordinates cover over Lincoln

Date

The API will only return a single month of data with each query but that's not an issue as the API can be called numerous times. I struggled here with my python knowledge and caved and asked lumo to help. I think due to the query needing "YYYY-MM" I just struggled to iterate through. I chose 2023-01 as it was the first full year it reliably returned data.


Outcomes

2023-01 to 2025-10

If we assume best case scenario where; under investigation, status update unavailable, court result unavailable and awaiting court outcome are all outcomes where somebody is found and prosecuted, then only 13.5% of burglaries somebody was prosecuted (it's likely much lower due to pending outcomes). Interestingly in 69% of reported cases they didn't even find a suspect - Intuitively this makes sense but frustrating nonetheless.

Investigation complete; no suspect identified 910 Unable to prosecute suspect 228 Court result unavailable 70 Status update unavailable 52 Under investigation 35 Awaiting court outcome 14 Further investigation is not in the public interest 4 Offender given a caution 2 Local resolution 2 Formal action is not in the public interest 1

2023-01 to 2023-12

If I run the same script over just 2023 then the percentage is still only around 14% that are solved and justice served. So potentially the rates might not change overtime.

Investigation complete; no suspect identified 388 Unable to prosecute suspect 78 Court result unavailable 38 Status update unavailable 36 Further investigation is not in the public interest 1 Offender given a caution 1

Total Crime 2023-01 to 2025-10

I ran the query on the outcome of all crime in Lincoln are between Jan 2023 and October 2025. The vast majority of crime goes unsolved (92%). Unable to prosecute being the top.

*Unable to prosecute suspect 16734 *Investigation complete; no suspect identified 13335 Court result unavailable 3770 Status update unavailable 1676 Under investigation 1252 Awaiting court outcome 977 *Further investigation is not in the public interest 945 Offender given a caution 411 Action to be taken by another organisation 377 Local resolution 328 *Formal action is not in the public interest 169 Offender given penalty notice 37 Suspect charged as part of another case 21

I was then interested in the unsolved rates for each crime individually over the same period. So I picked out the clear unsolved metrics (listed with an Asterix in the above table) and calculated an unsolved rate for each of the types of crime listed as a percentage.

theft-from-the-person 93.75% other-theft 91.11% vehicle-crime 90.68% bicycle-theft 88.84% burglary 86.72% criminal-damage-arson 86.22% violent-crime 81.38% robbery 78.02% other-crime 72.48% public-order 69.82% shoplifting 64.79% drugs 55.33% possession-of-weapons 42.33%

Something fun to start learning. I'm pretty shocked at the solve/prosecute rate for a lot of these crimes. Most crimes are north of 80%. It also makes sense crimes where you're caught in the act (drugs, shoplifting, possessing a weapon) have a higher rate of actually finding the person who carried out the crime, as they're likely right in front of the copper.

Overall seems pretty awful if you ask me. I'd be interested in seeing where your city sits.

Not sure where to take this project next, maybe plotting crime rates per month and learning more. Maybe a moving heatmap/gif of how crime might move around month to month. Going to play with the data more and more to see if there is anything I can see.


Raw Code (don't judge, pointers welcome)

import requests import pandas as pd import ast import matplotlib.pyplot as plt from datetime import datetime TL_coord_lat, TL_coord_long = 53.265001, -0.625169 TR_coord_lat, TR_coord_long = 53.257847, -0.484737 BR_coord_lat, BR_coord_long = 53.169748, -0.488852 BL_coord_lat, BL_coord_long = 53.173910, -0.632627 output = [] def get_data(month): url = f"https://data.police.uk/api/crimes-street/all-crime?date={month}&poly={TL_coord_lat},{TL_coord_long}:{TR_coord_lat},{TR_coord_long}:{BR_coord_lat},{BR_coord_long}:{BL_coord_lat},{BL_coord_long}" response = requests.get(url) data = response.json() for each in data: #print(each) try: outcome = each["outcome_status"]["category"] except (KeyError, TypeError): outcome = None date = month category = each["category"]1 s = s.replace(year=s.year + 1, month=1) else: s = s.replace(month=s.month + 1) #AI for m in month_range("2023-01", "2025-10"): get_data(m) cols = ['Month', 'Category', 'OutcomeStatus', 'Latitude', 'Longitude'] df = pd.DataFrame(output, columns=cols) counts = df[df["Category"] == "burglary"]["OutcomeStatus"].value_counts() print(counts)
print(df["OutcomeStatus"].value_counts()) percentage = counts / counts.sum() * 100 print(percentage) counts.plot(kind='bar') plt.title("Outcome Status for Burglary Cases") plt.xlabel("Outcome Status") plt.ylabel("Count") plt.xticks(rotation=45, ha='right') plt.tight_layout() plt.show()
# List of outcomes marked with * outcomes_with_asterisk = [ 'Unable to prosecute suspect', 'Investigation complete; no suspect identified', 'Further investigation is not in the public interest', 'Further action is not in the public interest', 'Formal action is not in the public interest' ] #AI result = df.groupby('Category').apply(lambda group: (group['OutcomeStatus'].isin(outcomes_with_asterisk)).mean() * 100 ) result_sorted = result.sort_values(ascending=False).round(2).astype(str) + '%' print(result_sorted)
Read Entire Article