Querying the Police UK API - Lincoln, UK Crime rates

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/


Breaking down the query

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%

Conclusion

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)

More from Balias
All posts