geolintr
Stop silently joining datasets with mismatched ONS geography boundaries.
pip install geolintr
Requires Python 3.9+ · Zero dependencies · Built for UK government and NHS data work
The problem
Every UK government dataset uses ONS geography codes. But those codes change — sometimes quietly and sometimes dramatically:
- CCGs became ICBs in July 2022 (106 areas became 42)
- LSOA and MSOA boundaries were redesigned after Census 2021
- Local authority districts merge and split regularly
- Terminated postcodes still appear in older datasets
When you join two datasets with codes from different years, the join silently succeeds but the results are wrong. geolintr catches this before it causes problems.
Quick start
from geolintr import detect_vintages, format_report
# Check a list of codes from your dataset
codes = ["E38000240", "E38000244", "E54000028", "E54000029"]
warnings = detect_vintages(codes)
print(format_report(warnings))
Output:
============================================================
geolintr report
============================================================
2 error(s) 0 warning(s) 1 info(s)
============================================================
[ERROR] 2 CCG code(s) found (E38 prefix). CCGs were abolished on 1 July 2022 and replaced by ICBs (E54).
Codes: E38000240, E38000244
Tip: Use geolintr.map_ccg_to_icb() to convert CCG codes to their ICB successors.
[ERROR] Both CCG (pre-2022) and ICB (post-2022) codes found. These represent different organisational boundaries.
Codes: E38000240, E38000244, E54000028, E54000029
Tip: Standardise to ICB using geolintr.map_ccg_to_icb().
Detect geography type
from geolintr import detect_geo_type
info = detect_geo_type("E38000240")
info.geo_type # "CCG"
info.description # "CCG (England) - deprecated 2022"
info.warnings # ["E38000240 appears to be a CCG code. CCGs were replaced by ICBs in July 2022."]
info = detect_geo_type("E54000028")
info.geo_type # "ICB"
info.is_known # True
Map CCG codes to ICB
from geolintr import map_ccg_to_icb
result = map_ccg_to_icb("E38000240")
result.found # True
result.target_code # "E54000029"
result.message # "Mapped CCG E38000240 to ICB E54000029."
Map any codes using a custom lookup
from geolintr import map_codes
# e.g. after a LAD merger
old_to_new = {
"E07000187": "E06000066", # Kettering -> North Northamptonshire
"E07000188": "E06000066", # Corby -> North Northamptonshire
}
results = map_codes(my_lad_codes, old_to_new, on_missing="warn")
for r in results:
if not r.found:
print(f"No mapping for {r.source_code}")
Validate codes against a known set
from geolintr import validate_codes
valid_icbs = {"E54000028", "E54000029", "E54000030"}
result = validate_codes(my_codes, valid_icbs)
result["coverage"] # 0.94 — 94% of codes matched
result["invalid"] # list of unrecognised codes
result["invalid_count"] # how many failed
What changed and when
from geolintr import boundary_changes_between
changes = boundary_changes_between(2021, 2023)
for c in changes:
print(f"{c['year']}: {c['description']}")
# 2021: LAD restructure - several mergers in England
# 2022: CCGs abolished, replaced by ICBs
# 2023: LSOA/MSOA boundaries updated for Census 2021
# 2023: Further LAD restructure
Supported geography types
| Prefix | Type | Notes |
|---|---|---|
| E01/W01/N01 | LSOA | Redesigned after Census 2021 |
| E02/W02 | MSOA | Redesigned after Census 2021 |
| E06-E09/W06/S12/N09 | LAD | Changes annually |
| E38 | CCG | Abolished July 2022 |
| E54 | ICB | Replaced CCGs July 2022 |
| E40 | NHS England Region | Stable |
| E12 | Region | Stable |
| E92/W92/S92/N92 | Country | Stable |
Why geolintr
If you work with UK open data, you have almost certainly joined datasets with mismatched boundaries without realising. The results look plausible, but the numbers are wrong. geolintr makes this class of error visible and fixable.
Built by someone who hits this issue everyday.
License
MIT
Metadata
Release files for geolintr 1.0.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| geolintr-1.0.1.tar.gz | 11.9 kB | Without provenance | Details for geolintr-1.0.1.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| geolintr-1.0.1-py3-none-any.whl | Python 3 | none | any | Details for geolintr-1.0.1-py3-none-any.whl |
Total release size: 22.4 kB
Release files / geolintr-1.0.1.tar.gz
| Download URL | geolintr-1.0.1.tar.gz |
|---|---|
| Size | 11.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9053957ac0aa8d6c89728969bb306c590d3a112c8b61728abb2a894f6aada3b2
|
|
BLAKE2b-256 checksum How to use checksums |
8ac92ba59cd33b080e3e909b230d231f2613a368b697b2b4704e585e1b141480
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.6
|
Release files / geolintr-1.0.1-py3-none-any.whl
| Download URL | geolintr-1.0.1-py3-none-any.whl |
|---|---|
| Size | 10.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
2749ef5afbed3d900f7fb6878f205863fe20ad0ea71ca5917c2aac50936b6278
|
|
BLAKE2b-256 checksum How to use checksums |
a44e94ee4664a69d0f5c3d02fe4916a34ccaabc38e069c9b7afb387aba24ba8e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.6
|
Security for geolintr 1.0.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.