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
Functions
Return the download URL for the most recent known PACE annual workbook. |
|
|
Parse Table 1 (monthly stop & search counts) from a PACE Excel workbook. |
|
Parse Table 2 (quarterly PACE arrests) from a PACE Excel workbook. |
|
Download and return the latest PACE statistics. |
|
Validate a PACE DataFrame for structural integrity. |
Module Contents
- 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:
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
- bolster.data_sources.psni.pace.parse_stop_search(file_path)[source]
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 categorymetric:"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 categorycount: 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:
- Returns:
DataFrame — see
parse_stop_search()orparse_arrests()for column descriptions.- Raises:
ValueError – If
breakdownis not"stop_search"or"arrests".PSNIDataNotFoundError – If the download fails.
PSNIValidationError – If the workbook structure is not as expected.
- Return type:
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:
df (pandas.DataFrame) – DataFrame returned by
parse_stop_search()orparse_arrests().breakdown (str) –
"stop_search"or"arrests"— selects the expected column set.
- Returns:
Trueif the DataFrame passes all checks.- Raises:
PSNIValidationError – If any check fails (empty DataFrame, missing columns, non-positive counts).
- Return type:
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