Upload two spatial datasets and interactively run buffers, spatial joins and nearest-neighbor queries — with a live map and downloadable results.
spatial-join-lab is a small Streamlit app (plus a cleanly separated, testable core library) for hands-on spatial analysis. Load two vector datasets — or the built-in Oslo sample — pick an operation, and see the result on an interactive map you can download as GeoJSON or GeoParquet. It is a working demo of the analysis workflows taught on python-geospatial.com.
Maintained by python-geospatial.com, a knowledge base for the modern Python geospatial stack.
Buffers, spatial joins and nearest-neighbor queries are the everyday verbs of GIS, but they are also where people quietly get the units wrong — buffering in degrees, or measuring distances in Web Mercator metres that are stretched by tens of percent. This lab does the metric work correctly by auto-selecting a UTM zone from your data and shows you which EPSG code it used, so the right way becomes the obvious way.
Not published to PyPI — install straight from the repository:
pip install "git+https://github.com/python-geospatial/spatial-join-lab.git"Or clone and install in editable mode:
git clone https://github.com/python-geospatial/spatial-join-lab.git
cd spatial-join-lab
pip install -e ".[dev]"spatial-join-lab # launches Streamlit
# or, equivalently
python -m spatial_join_lab
# or point Streamlit at the bundled app directly
streamlit run src/spatial_join_lab/app.pyStreamlit opens a browser tab. The sidebar walks you through it:
- Data — click Load sample data (Oslo sensors + parcels), or upload Dataset A
and Dataset B. Supported formats: GeoJSON, GeoPackage (
.gpkg), GeoParquet (.parquet) and zipped Shapefiles (.zip). - Operation — pick Buffer, Spatial Join or Nearest Neighbor and set its parameters (buffer distance in metres; join predicate + how; k + optional max distance).
- Click Run. The map shows Dataset A (blue), Dataset B (green) and the result (red), a table previews the output, and download buttons export GeoJSON / GeoParquet.
A green banner reports the projected CRS used for metric work, e.g.:
Metric operations used EPSG:32632 (an auto-selected UTM zone from your data
centroid) — not Web Mercator, whose distances are distorted.
The analysis functions are import-safe (no Streamlit needed):
from spatial_join_lab import operations, sample_data
sensors, parcels = sample_data.load_sample()
# Which metric CRS fits this data?
_, epsg = operations.to_metric_crs(sensors)
print(epsg) # 32632 (UTM 32N, central Oslo)
# 100 m buffers around each sensor (computed in metres, returned in EPSG:4326)
zones = operations.buffer(sensors, distance_m=100)
# Which parcel does each sensor fall in?
joined = operations.spatial_join(sensors, parcels, predicate="intersects", how="inner")
print(joined[["sensor_id", "parcel_id", "land_use"]].to_string(index=False))
# sensor_id parcel_id land_use
# S1 P1 residential
# S2 P2 commercial
# S3 P3 park
# S4 P4 industrial
# Nearest parcel to each sensor, with true metre distances
near = operations.nearest_neighbor(sensors, parcels, k=1)
print(near[["sensor_id", "parcel_id", "distance_m"]].round(1))- Correct CRS handling. Metric operations run in an auto-selected UTM zone (northern
326xx/ southern327xx), never Web Mercator. The chosen EPSG is surfaced in the UI. - Three core operations. Buffer, spatial join (intersects / within / contains / overlaps; inner / left / right), and k-nearest-neighbor with optional max-distance.
- Real distances. Nearest-neighbor output carries a
distance_mcolumn in metres. - Many formats in. GeoJSON, GeoPackage, GeoParquet and zipped Shapefiles, from an upload or a path.
- Downloadable results. Export any result as GeoJSON or GeoParquet.
- Testable core. All logic lives in
operations.pywith no Streamlit import, covered by a network-free pytest suite.
to_metric_crs reprojects the data to EPSG:4326, takes the centroid to get a
longitude/latitude, derives the UTM zone (zone = int((lon + 180) / 6) + 1), and picks
32600 + zone (north) or 32700 + zone (south). Buffers and nearest-neighbor distances
are computed in that projected CRS and the geometry is returned in EPSG:4326. Spatial
joins align both layers' CRS first and delegate to geopandas.sjoin; nearest-neighbor
uses geopandas.sjoin_nearest with distance_col="distance_m". Transformers follow
always_xy=True discipline to avoid axis-order surprises.
Deep dives on python-geospatial.com behind each operation in this lab:
- Spatial Joins & Merging
- Proximity & Buffer Analysis
- Nearest Neighbor & KD-Tree Search
- Geometric Intersections & Overlays
- Spatial analysis & advanced query techniques (hub)
git clone https://github.com/python-geospatial/spatial-join-lab.git
cd spatial-join-lab
pip install -e ".[dev]"
ruff check .
pytest -qThe test suite runs without network access and without launching Streamlit.
MIT © 2026 python-geospatial.com