A spatial query layer for Polars. Rust core, Python API.
[!NOTE] Highly competitive on Apache SpatialBench (single-node spatial query benchmark): fastest on 11/24 testcases, within 5% of the fastest time on 14/24 testcases
Apache SpatialBench SF1 · lower is better · bars past the cap truncated with their value · TIMEOUT / ERROR annotated
Installation
pip install pycanopy
Pre-built wheels for Linux, macOS, and Windows. No Rust toolchain required.
import polars as pl
from pycanopy import SpatialFrame
sf = SpatialFrame(pl.read_parquet("cities.parquet"), x_col="lon", y_col="lat")
result = sf.lazy().filter(pl.col("population") > 100_000).range_query(-10.0, 35.0, 40.0, 70.0).collect()
Why PyCanopy
The driving motivator behind creating this library was to provide the optimizations of relational DBs (query planning, indexing, etc) in a fast, Polars-like interface meant for in-memory spatial work.
| Capability | PyCanopy | GeoPandas | DuckDB | SedonaDB | Spatial Polars |
|---|---|---|---|---|---|
| Uses Polars DataFrames directly | ✓ | ✗ | ✗ | ✗ | ✓ |
| Spatial-aware query planning | ✓ | ✗ | ✓ | ✓ | ✗ |
| Automatically accelerates spatial joins with an index | ✓ | ✓ | ✓ | ✓ | ✗ |
| Explicit cost-based choice between scanning and building an index | ✓ | ✗ | ✗ | ✗ | ✗ |
| Selects among multiple spatial index types by workload | ✓ | ✗ | ✗ | ✗ | ✗ |
Example Operations
Optimized range query
lf = (
sf.lazy()
.range_query(min_x=-10.0, min_y=35.0, max_x=40.0, max_y=70.0)
.filter(pl.col("population") > 100_000)
)
print(lf.explain())
# RANGE_QUERY [(-10, 35) → (40, 70)]
# FROM
# FILTER [(col("population")) > (dyn int: 100000)]
# FROM
# DF [N=100,000; path: EXPR]
The optimizer runs the scalar filter first. On the EXPR path, the surviving original row indices are passed to Rust, which returns a spatial Boolean mask over those candidates.
kNN join
query_df = pl.DataFrame({"qx": [2.35, 13.4], "qy": [48.85, 52.5]})
result = sf.lazy().knn_join(query_df, x_col="qx", y_col="qy", k=3).collect()
For each row in query_df, returns the 3 nearest rows in the SpatialFrame. Large probes are streamed in morsels automatically.
Point-in-polygon join with aggregation
import pycanopy as pc
zones = SpatialFrame.from_wkb_polygons(
pl.read_parquet("zones.parquet"),
geometry_col="geometry",
)
trips = pl.read_parquet("trips.parquet")
stats = (
zones.lazy()
.within_join(trips, x_col="lon", y_col="lat")
.group_by(["zone_id"])
.agg(trip_count=pc.agg.count(), avg_fare=pc.agg.mean("fare"))
)
Each query-side batch is joined and aggregated before the next begins, so the complete pair frame is never materialized.
[!NOTE] For the full operation catalog, index modes, streaming joins, and API reference see the docs site.
Benchmarks
Apache SpatialBench
Run on a single m7i.2xlarge (8 vCPU, 32 GB), the same hardware used by Apache SpatialBench. PyCanopy is measured live with index_mode="auto". Results were produced using the benchmark harness in bench/spatial_bench.
PyCanopy is fastest on 11/24 testcases and lands within 5% of the fastest time on 14/24 testcases (there is some variance among benchmark runs).
SF1 (~6M trips)
Apache SpatialBench SF1 · lower is better · linear axis, bars past the cap truncated with their value · TIMEOUT / ERROR annotated
SF10 (~60M trips)
Apache SpatialBench SF10 · lower is better · linear axis, bars past the cap truncated with their value · TIMEOUT / ERROR annotated
SedonaDB, DuckDB, and GeoPandas baselines come from published SpatialBench results. See the full per-query results and methodology.
How It Works
Spatial-aware Logical Planning
- Predicate pushdown: moves compatible scalar filters ahead of spatial predicates
- Filter fusion: combines eligible range and contains predicates in a single Rust call
- Projection pushdown: narrows join inputs before gathering rows
- Join orientation: flips supported joins based on relative input sizes
Physical Planning
- IO path: directly queries the spatial index and slices source rows for selective predicates
- EXPR path: applies scalar filters in Polars before evaluating broader spatial predicates in Rust
- Morsel streaming: processes large query-side joins in batches for incremental collection, lazy pipelines, or direct Parquet sinks
- Streaming aggregation: aggregates each join batch as it is produced, avoiding materialization of the complete join result
Automatic Indexing
- Tracks dataset extent, spatial distribution, and density to estimate query selectivity
- Uses a cost model to compare brute-force scanning, reusing an existing index, and building and probing a new index
- Selects among grid, KD-tree, and R-tree indexes based on the query and workload
Why Rust
The hot paths benefit from packed immutable index structures, parallel loops, and efficient access to contiguous NumPy buffers. PyO3 and Maturin provide direct Python bindings and cross-platform extension packaging.
For a detailed overview of PyCanopy's design, see the How It Works documentation.
Acknowledgements
Some works that inspired this project:
- Polars: a columnar DataFrame engine that PyCanopy builds on
- geo-index: provides packed, immutable, zero-copy KD-tree and R-tree structures used
- Spatial Polars: an earlier effort to bring spatial functionality to Polars
- Apache Sedona: state-of-the-art spatial SQL engine + benchmark for evals
License
MIT
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pycanopy-0.4.0.tar.gz.
File metadata
- Download URL: pycanopy-0.4.0.tar.gz
- Upload date:
- Size: 409.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bc83fcd3448d1abcc79d1dca0b963bb913f509ac868de20b6396e9b087e94ba0
|
|
| MD5 |
599c29527492f0f53e9feca2a6f7e652
|
|
| BLAKE2b-256 |
c1543967e6117ced7df87c1b51f5d3b29d93e7940de106dc773a3ed344d770af
|
Provenance
The following attestation bundles were made for pycanopy-0.4.0.tar.gz:
Publisher:
release.yml on pranav-walimbe/PyCanopy
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pycanopy-0.4.0.tar.gz -
Subject digest:
bc83fcd3448d1abcc79d1dca0b963bb913f509ac868de20b6396e9b087e94ba0 - Sigstore transparency entry: 2595488348
- Sigstore integration time:
-
Permalink:
pranav-walimbe/PyCanopy@5020cde56dbcf08773f22fbec6dbe67701c802d9 -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/pranav-walimbe
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@5020cde56dbcf08773f22fbec6dbe67701c802d9 -
Trigger Event:
push
-
Statement type:
File details
Details for the file pycanopy-0.4.0-cp310-abi3-win_amd64.whl.
File metadata
- Download URL: pycanopy-0.4.0-cp310-abi3-win_amd64.whl
- Upload date:
- Size: 868.1 kB
- Tags: CPython 3.10+, Windows x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fac4ee80070dfec327b1332e813eb5410c4cc5183fa7ad8776ca7875964343ea
|
|
| MD5 |
942247e46c3c6108b6c4d060aeacbfe2
|
|
| BLAKE2b-256 |
79a7d9c76a6699b111d8d6593f4902d357f08f6b87bab85b82126006724a1383
|
Provenance
The following attestation bundles were made for pycanopy-0.4.0-cp310-abi3-win_amd64.whl:
Publisher:
release.yml on pranav-walimbe/PyCanopy
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pycanopy-0.4.0-cp310-abi3-win_amd64.whl -
Subject digest:
fac4ee80070dfec327b1332e813eb5410c4cc5183fa7ad8776ca7875964343ea - Sigstore transparency entry: 2595490501
- Sigstore integration time:
-
Permalink:
pranav-walimbe/PyCanopy@5020cde56dbcf08773f22fbec6dbe67701c802d9 -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/pranav-walimbe
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@5020cde56dbcf08773f22fbec6dbe67701c802d9 -
Trigger Event:
push
-
Statement type:
File details
Details for the file pycanopy-0.4.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.
File metadata
- Download URL: pycanopy-0.4.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- Upload date:
- Size: 1.0 MB
- Tags: CPython 3.10+, manylinux: glibc 2.17+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cc10c83de81a2374390f8ded47198113be041879a52bb3a2e77c2abf41f1c271
|
|
| MD5 |
751bc20fc9e2b7246dd46de0fca9c1e5
|
|
| BLAKE2b-256 |
c0d57df727f4679ab7cfb615e93cd9737f3817cf268cd8701dfa1995bd36ef62
|
Provenance
The following attestation bundles were made for pycanopy-0.4.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl:
Publisher:
release.yml on pranav-walimbe/PyCanopy
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pycanopy-0.4.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl -
Subject digest:
cc10c83de81a2374390f8ded47198113be041879a52bb3a2e77c2abf41f1c271 - Sigstore transparency entry: 2595488974
- Sigstore integration time:
-
Permalink:
pranav-walimbe/PyCanopy@5020cde56dbcf08773f22fbec6dbe67701c802d9 -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/pranav-walimbe
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@5020cde56dbcf08773f22fbec6dbe67701c802d9 -
Trigger Event:
push
-
Statement type:
File details
Details for the file pycanopy-0.4.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.
File metadata
- Download URL: pycanopy-0.4.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
- Upload date:
- Size: 944.8 kB
- Tags: CPython 3.10+, manylinux: glibc 2.17+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
eed91dfb233aec4641d203ba412fa07c763ebae7f392e80d9bcdc9829360ca1c
|
|
| MD5 |
29fe26dc58ea90e4e6d009be7030ad15
|
|
| BLAKE2b-256 |
0469cfbacb55610c2e1869ca655a166a0285aa6b6d5303eed1a314641df7d104
|
Provenance
The following attestation bundles were made for pycanopy-0.4.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl:
Publisher:
release.yml on pranav-walimbe/PyCanopy
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pycanopy-0.4.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl -
Subject digest:
eed91dfb233aec4641d203ba412fa07c763ebae7f392e80d9bcdc9829360ca1c - Sigstore transparency entry: 2595490279
- Sigstore integration time:
-
Permalink:
pranav-walimbe/PyCanopy@5020cde56dbcf08773f22fbec6dbe67701c802d9 -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/pranav-walimbe
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@5020cde56dbcf08773f22fbec6dbe67701c802d9 -
Trigger Event:
push
-
Statement type:
File details
Details for the file pycanopy-0.4.0-cp310-abi3-macosx_11_0_arm64.whl.
File metadata
- Download URL: pycanopy-0.4.0-cp310-abi3-macosx_11_0_arm64.whl
- Upload date:
- Size: 901.7 kB
- Tags: CPython 3.10+, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
71fccd663f4ae3e301e52951fb09fb9b2c5ead50c050bb9c1178291b18b42ab8
|
|
| MD5 |
e865f3512fa46c06dad5786e97aa0555
|
|
| BLAKE2b-256 |
17e3b5812291993fad9ae1197eedcaa951191cbf11c1fdd1b88d9cc7f835733b
|
Provenance
The following attestation bundles were made for pycanopy-0.4.0-cp310-abi3-macosx_11_0_arm64.whl:
Publisher:
release.yml on pranav-walimbe/PyCanopy
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pycanopy-0.4.0-cp310-abi3-macosx_11_0_arm64.whl -
Subject digest:
71fccd663f4ae3e301e52951fb09fb9b2c5ead50c050bb9c1178291b18b42ab8 - Sigstore transparency entry: 2595490914
- Sigstore integration time:
-
Permalink:
pranav-walimbe/PyCanopy@5020cde56dbcf08773f22fbec6dbe67701c802d9 -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/pranav-walimbe
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@5020cde56dbcf08773f22fbec6dbe67701c802d9 -
Trigger Event:
push
-
Statement type:
File details
Details for the file pycanopy-0.4.0-cp310-abi3-macosx_10_12_x86_64.whl.
File metadata
- Download URL: pycanopy-0.4.0-cp310-abi3-macosx_10_12_x86_64.whl
- Upload date:
- Size: 959.5 kB
- Tags: CPython 3.10+, macOS 10.12+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
77bd96509379feb440ca255b0928270cfe22dc8d0450daf2b5affd702782afbe
|
|
| MD5 |
19068dfe12ed6ad8535baf6f867df25f
|
|
| BLAKE2b-256 |
bd954430d0b6aa258c3f3211c30e5c8ebb16bbed34d374eca293049dbc479fc3
|
Provenance
The following attestation bundles were made for pycanopy-0.4.0-cp310-abi3-macosx_10_12_x86_64.whl:
Publisher:
release.yml on pranav-walimbe/PyCanopy
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pycanopy-0.4.0-cp310-abi3-macosx_10_12_x86_64.whl -
Subject digest:
77bd96509379feb440ca255b0928270cfe22dc8d0450daf2b5affd702782afbe - Sigstore transparency entry: 2595489142
- Sigstore integration time:
-
Permalink:
pranav-walimbe/PyCanopy@5020cde56dbcf08773f22fbec6dbe67701c802d9 -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/pranav-walimbe
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@5020cde56dbcf08773f22fbec6dbe67701c802d9 -
Trigger Event:
push
-
Statement type: