bolster.data_sources.psni.pace

PSNI Police and Criminal Evidence (PACE) Order Statistics.

Provides access to annual PACE statistics for Northern Ireland, covering: - Stop and search activity (monthly counts by reason: stolen articles, offensive

weapons/blade or point, going equipped/prohibited articles, fireworks)

  • Arrests under PACE by quarter, gender, and whether a solicitor or friend/relative was requested during detention

Each annual Excel workbook covers a single financial year (April–March) and is published by PSNI Statistics Branch each May on the PSNI publications index:

URL discovery note: The PSNI publications index page is protected by Cloudflare and cannot be scraped programmatically. Direct asset URLs at /sites/default/files/ can be fetched with a browser-like User-Agent + Referer header, but the filename portion of the URL is not predictable (includes the year in YYYY.YY format and may include a revision suffix such as a2).

The PACE_URLS dict therefore hard-codes confirmed download URLs. It should be updated each May when PSNI publishes the new edition. Use get_latest_pace_url() to retrieve the most recent known URL.

Data Source:

PSNI Statistics Branch https://www.psni.police.uk/about-us/our-publications-and-reports/official-statistics/police-and-criminal-evidence-pace-order

Update Frequency: Annual (published each May) Geographic Coverage: Northern Ireland (NI-wide aggregate) Time Coverage: One financial year per workbook; PACE_URLS spans 2024/25–2025/26

Example

>>> from bolster.data_sources.psni import pace
>>> url = pace.get_latest_pace_url()
>>> url.startswith("https://")
True
>>> df = pace.get_latest_pace(breakdown="stop_search")
>>> "reason" in df.columns
True
>>> pace.validate_pace(df, "stop_search")
True

Attributes

logger

PACE_URLS

Functions

get_latest_pace_url()

Return the download URL for the most recent known PACE annual workbook.

parse_stop_search(file_path)

Parse Table 1 (monthly stop & search counts) from a PACE Excel workbook.

parse_arrests(file_path)

Parse Table 2 (quarterly PACE arrests) from a PACE Excel workbook.

get_latest_pace([breakdown, force_refresh])

Download and return the latest PACE statistics.

validate_pace(df, breakdown)

Validate a PACE DataFrame for structural integrity.

Module Contents

bolster.data_sources.psni.pace.logger[source]
bolster.data_sources.psni.pace.PACE_URLS: dict[str, str][source]
bolster.data_sources.psni.pace.get_latest_pace_url()[source]

Return the download URL for the most recent known PACE annual workbook.

The URL is drawn from PACE_URLS. Update that dict each May when PSNI publishes a new edition.

Returns:

Direct download URL for the latest PACE Excel workbook.

Return type:

str

Example

>>> from bolster.data_sources.psni.pace import get_latest_pace_url
>>> url = get_latest_pace_url()
>>> url.startswith("https://www.psni.police.uk/")
True

Parse Table 1 (monthly stop & search counts) from a PACE Excel workbook.

The table covers stop and search activity for a single financial year, broken down by month (Apr–Mar) and search reason.

Parameters:

file_path (pathlib.Path | str) – Local path to the downloaded PACE Excel workbook.

Returns:

  • financial_year: e.g. "2025/26"

  • year: int, start year of financial year (e.g. 2025)

  • month: month abbreviation, e.g. "Apr"

  • reason: search reason category

  • metric: "Searches" or "Arrests"

  • count: integer count

Return type:

DataFrame with columns

Raises:

PSNIValidationError – If the expected table structure is not found.

Example

>>> import tempfile, pathlib
>>> df = parse_stop_search("/tmp/pace_2025_26.xlsx")  
>>> list(df.columns)  
['financial_year', 'year', 'month', 'reason', 'metric', 'count']
bolster.data_sources.psni.pace.parse_arrests(file_path)[source]

Parse Table 2 (quarterly PACE arrests) from a PACE Excel workbook.

The table covers arrests under PACE for a single financial year, broken down by quarter and category (total, male, female, unknown/other, and whether a solicitor or friend/relative was requested during detention).

Parameters:

file_path (pathlib.Path | str) – Local path to the downloaded PACE Excel workbook.

Returns:

  • financial_year: e.g. "2025/26"

  • year: int, start year of financial year (e.g. 2025)

  • quarter: quarter label, e.g. "Q1 (Apr–Jun)"

  • category: demographic/request category

  • count: integer count

Return type:

DataFrame with columns

Raises:

PSNIValidationError – If the expected table structure is not found.

Example

>>> df = parse_arrests("/tmp/pace_2025_26.xlsx")  
>>> list(df.columns)  
['financial_year', 'year', 'quarter', 'category', 'count']
bolster.data_sources.psni.pace.get_latest_pace(breakdown='stop_search', force_refresh=False)[source]

Download and return the latest PACE statistics.

Downloads the most recent PACE Excel workbook (from PACE_URLS), caches it locally for one year, and returns either the stop & search or the arrests breakdown.

Parameters:
  • breakdown (str) – Which table to return — "stop_search" (Table 1, monthly stop & search counts) or "arrests" (Table 2, quarterly arrest demographics). Default: "stop_search".

  • force_refresh (bool) – If True, bypass the cache and re-download. Default: False.

Returns:

DataFrame — see parse_stop_search() or parse_arrests() for column descriptions.

Raises:
Return type:

pandas.DataFrame

Example

>>> df = get_latest_pace(breakdown="stop_search")  
>>> "reason" in df.columns
True
>>> df = get_latest_pace(breakdown="arrests")  
>>> "category" in df.columns
True
bolster.data_sources.psni.pace.validate_pace(df, breakdown)[source]

Validate a PACE DataFrame for structural integrity.

Checks that the DataFrame has the required columns and contains at least some data.

Parameters:
Returns:

True if the DataFrame passes all checks.

Raises:

PSNIValidationError – If any check fails (empty DataFrame, missing columns, non-positive counts).

Return type:

bool

Example

>>> import pandas as pd
>>> from bolster.data_sources.psni.pace import validate_pace, PSNIValidationError
>>> validate_pace(pd.DataFrame(), "stop_search")
Traceback (most recent call last):
    ...
bolster.data_sources.psni._base.PSNIValidationError: PACE DataFrame is empty