Close

26th July 2022

Police API Stop and Search lookup with Streamlit

Police API Stop and Search lookup with Streamlit

Following on from the quick API demo I built for the Vehicle Enquiry Service API, I wanted to revisit some work I’d previously done that used the UK Police Data API along with metabase and other technologies to help interrogate public policing data.

Using the streamlit library to provide an interface for querying UK Police stop and search data.

As with the previous code that queried an API the application needs a mechanism to query the API, process the returned data and display the returning information.  Further the application should also take inputs from a user, that specify Forces and date ranges to be looked up.

The Streamlit library is an incredible resource for building and rapidly sharing data-based applications.  Streamlit will provide the layout, text inputs, buttons, and seamless integrations to popular libraries.

The Pandas library provides ready to use data structures and data analysis tools. The Requests library is the de facto standard for making HTTP requests in Python, so the application will use this to interact with the UK Police Data API.  Altair and pydeck are libraries that provide engaging ways of presenting the data returned from any query.

All together the application uses Python, with Streamlit, Pandas, Requests, Datetime, Altair and pydeck and the UK Police Data API.

Using Anaconda I can create a Python environment that contains only the libraries that the application needs, and avoids creating issues with my local copies of python used by the OS for my device.  With anaconda available I can either use Anaconda Navigator to import the required libraries to the virtual python environment or I can open the virtual environment from the command line and interact directly using pip.  For example, to install streamlit, I could run the following from the virtual environment command line.

pip install streamlit

With the libraries available the python script that streamlit will call can be created and we can launch streamlit from the virtual environment command line.

streamlit run app.py

 

The Python Script

Rather than go through line by line what the script is doing in this post I’ve commented the code and also made the project available on github for review.

import streamlit as st
import pandas as pd
from pandas import json_normalize
import requests
import datetime
import altair as alt
import pydeck as pdk


def police_api():
    
    def police_query():
   
        st.info("Change the selection below to set different parameters for the API query.")

# By building a df object from the returned json values we can use these in a selectbox, capturing the st.session_state key allows us to pass the value back to the API query string
# On value change updates the state and recalls the function
        st.selectbox(
        'Select the Police Force',
        (force_json_df["id"].unique()), key="force", on_change=police_query)
        st.session_state.force

# We can do something similar with the date, but we need to transform the datetime returned value to YYYY-MM format, later rather than calling the key we're going to call the variable that the key updates, because then we can get it in the correct format
# on value change updates the state recalls the function
        date = st.date_input("Select a date", datetime.date(2020, 1, 1),  key="qdate", on_change=police_query)
        date = date.strftime("%Y-%m")
        st.write(date)

# This is better, passing the force and date time values into the API query string for stop and search
        url = (f'https://data.police.uk/api/stops-force?force={st.session_state.force}&date={date}')
        
# error check write the API query string to the page
        st.markdown(f'<h1 style="color:#6ac94f;font-size:12px;">The Police API Query String URL = {url}</h1>', unsafe_allow_html=True)
        
# execute the API call and build the Stop and search df added simple error handling to catch the error if the API call fails   
        try: 
            res = requests.get(url)
            j = res.json()
            police = json_normalize(j)

# rebuild a smaller dataframe with just the plotable columns to pass to st.pydeck_chart
# some error checking to only pull in numeric values and to drop na values in lat column, 
            police["lat"] = police["location.latitude"]
            police['lat'] = pd.to_numeric(police['lat'])
            police["lon"] = police["location.longitude"]
            police['lon'] = pd.to_numeric(police['lon'])
            df = pd.DataFrame(police, columns=['lat', 'lon'])
            df.dropna(subset=['lat'], inplace=True)

# build the st.pydeck_chart configuration and display the map
            st.markdown(f'<h1 style="font-size:18px;">Stop and Search Data Map for {st.session_state.force, date}</h1>', unsafe_allow_html=True)
            st.pydeck_chart(pdk.Deck(
            map_style='mapbox://styles/mapbox/light-v9',
            initial_view_state=pdk.ViewState(
                latitude=51.50,
                longitude=-0.1276,
                zoom=7,
                pitch=40,
            ),
            layers=[
                pdk.Layer(
                    'HexagonLayer',
                    data=df,
                    get_position='[lon, lat]',
                    radius=200,
                    elevation_scale=4,
                    elevation_range=[0, 1000],
                    pickable=True,
                    extruded=True,
                ),
                pdk.Layer(
                    'ScatterplotLayer',
                    data=df,
                    get_position='[lon, lat]',
                    get_color='[200, 30, 0, 160]',
                    get_radius=200,
                ),
            ],
            ))

# show the full df from the API call
            st.markdown(f'<h1 style="font-size:18px;">Stop and Search Data Table for {st.session_state.force, date}</h1>', unsafe_allow_html=True)
            st.dataframe(police)

# download function for df
            def convert_df(police):
                return police.to_csv().encode('utf-8')

            csv = convert_df(police)

            st.download_button(
                label="Download Output as CSV",
                data=csv,
                file_name=(f'StopandSearch_{st.session_state.force}_{date}.csv'),
                mime='text/csv',
                )

# build and show and altair chart
            st.markdown(f'<h1 style="font-size:18px;">Stop and Search Data Chart, age_range and object_of_search {st.session_state.force, date}</h1>', unsafe_allow_html=True)
            chart = alt.Chart(police).mark_area().encode(
            alt.X('age_range:N'),
            alt.Y('count(kind):Q'),
            color='object_of_search:N'
            ).properties(
                width=500,
                height=500
            )
            st.altair_chart(chart, use_container_width=True)
# Error handling for the API call, error appears if nothing returned by JSON stream
        except:
            st.error("Error, please check the API query string - no data returned for the selected Police Force and date")

    st.info("Change the selection below to set different parameters for the API query.")
# To pass a force into the stop and search API we need to capture the force IDs from the police API.  
    force = requests.get("https://data.police.uk/api/forces")
    force_json = force.json()
    force_json_df = json_normalize(force_json)
# By building a df object from the returned json values we can use these in a selectbox, capturing the st.session_state key allows us to pass the value back to the API query string
# On value change updates the state and calls the function above
    st.selectbox(
        'Select the Police Force',
        (force_json_df["id"].unique()), key="force", on_change=police_query)
    st.session_state.force

# We can do something similar with the date, but we need to transform the datetime returned value to YYYY-MM format, later rather than calling the key we're going to call the variable that the key updates, because then we can get it in the correct format
# on value change updates the state calls the function above
    date = st.date_input("Select a date", datetime.date(2020, 1, 1),  key="qdate", on_change=police_query)
    date = date.strftime("%Y-%m") 
    st.write(date)

# first API call, implementation without any variables, for the first select and date data inputs
    url = "https://data.police.uk/api/stops-force?force=avon-and-somerset&date=2020-01"

# error check write the API query string to the page
    st.markdown(f'<h1 style="color:#6ac94f;font-size:12px;">The Police API Query String URL = {url}</h1>', unsafe_allow_html=True)

# execute the API call and build the Stop and search df added simple error handling to catch the error if the API call fails 
    try:
        res = requests.get(url)
        j = res.json()
        police = json_normalize(j)

# rebuild a smaller dataframe with just the plotable columns to pass to st.pydeck_chart
# some error checking to only pull in numeric values and to drop na values in lat column, 
        police["lat"] = police["location.latitude"]
        police['lat'] = pd.to_numeric(police['lat'])
        police["lon"] = police["location.longitude"]
        police['lon'] = pd.to_numeric(police['lon'])
        df = pd.DataFrame(police, columns=['lat', 'lon'])
        df.dropna(subset=['lat'], inplace=True)

        st.markdown(f'<h1 style="font-size:18px;">Stop and Search Data Map for {st.session_state.force, date}</h1>', unsafe_allow_html=True)
# build the st.pydeck_chart configuration and display the map
        st.pydeck_chart(pdk.Deck(
        map_style='mapbox://styles/mapbox/light-v9',
        initial_view_state=pdk.ViewState(
            latitude=51.50,
            longitude=-0.1276,
            zoom=7,
            pitch=40,
        ),
        layers=[
            pdk.Layer(
                'HexagonLayer',
                data=df,
                get_position='[lon, lat]',
                radius=200,
                elevation_scale=4,
                elevation_range=[0, 1000],
                pickable=True,
                extruded=True,
                ),
            pdk.Layer(
                'ScatterplotLayer',
                data=df,
                get_position='[lon, lat]',
                get_color='[200, 30, 0, 160]',
                get_radius=200,
            ),
        ],
        ))
        

# show the df from the API call
        st.markdown(f'<h1 style="font-size:18px;">Stop and Search Data Table for {st.session_state.force, date}</h1>', unsafe_allow_html=True)
        st.dataframe(police)

# download function for the df
        def convert_df(police):
            return police.to_csv().encode('utf-8')
    
        csv = convert_df(police)

        st.download_button(
            label="Download Output as CSV",
            data=csv,
            file_name=(f'StopandSearch_{st.session_state.force}_{date}.csv'),
            mime='text/csv',
            )

# build and show and altair chart
        st.markdown(f'<h1 style="font-size:18px;">Stop and Search Data Chart, age_range and object_of_search {st.session_state.force, date}</h1>', unsafe_allow_html=True)
        chart = alt.Chart(police).mark_area().encode(
        alt.X('age_range:N'),
        alt.Y('count(kind):Q'),
        color='object_of_search:N'
        ).properties(
            width=500,
            height=500
        )
        st.altair_chart(chart, use_container_width=True)
# Error handling for the API call, error appears if nothing returned by JSON stream
    except:
        st.error("Error, please check the API query string - no data returned for the selected Police Force and date")

st.set_page_config(layout="wide", page_title="Police Data API Explorer")

st.title("Police Data API Explorer")

st.info("This is a Streamlit app that allows you to explore the Police Data API. Click the button in the side bar to start with Stop and Search data")

if st.button("Click to explore the Police Stop and Search Data API"):
    police_api()

 

Packaging the application

I covered how to package a streamlit application in a previous post, the process for packaging this is identical.

Video of running MVP

I appreciate looking at the code is a little dry, so here is a video (hosted on github) that showcases what the code above does.

The video might not load on mobile, and can be accessed from this link instead

Summary

The goal was to take what I learned through building a web based application to retrieve vehicle information from the UK DVLA Vehicle Enquiry Service API and apply that to the UK Police Data API.

A repository with the code required to build this is available on github.  The police data is open, so there isn’t even a requirement for a key to access the data.

No support offered or liability accepted, use is entirely at your own risk.

Hopefully this is interesting or useful to someone else!

Simon