Metadata-Version: 2.5
Name: entityraster
Version: 0.1.0
Summary: Raster-valued attributes for GeoPandas: one raster per row (a GeoRasterFrame), with explicit zero-vs-unknown semantics
Project-URL: Homepage, https://github.com/Urban-GeoInformatics/entityraster
Project-URL: Repository, https://github.com/Urban-GeoInformatics/entityraster
Project-URL: Issues, https://github.com/Urban-GeoInformatics/entityraster/issues
Project-URL: Documentation, https://entityraster.readthedocs.io
Project-URL: Changelog, https://github.com/Urban-GeoInformatics/entityraster/blob/main/CHANGELOG.md
Author-email: Kamyar Hasanzadeh <kamyar.hasanzadeh@helsinki.fi>
License-Expression: BSD-3-Clause
License-File: LICENSE
Keywords: activity space,exposure,extension-array,geopandas,geoparquet,gis,pandas,raster
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: BSD License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering :: GIS
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: geopandas>=1.0
Requires-Dist: matplotlib>=3.8
Requires-Dist: numpy>=1.26
Requires-Dist: pandas>=2.0
Requires-Dist: pyarrow>=14
Requires-Dist: rasterio>=1.3
Requires-Dist: scipy>=1.11
Requires-Dist: shapely>=2.0
Requires-Dist: xarray>=2023.1
Provides-Extra: docs
Requires-Dist: ipykernel; extra == 'docs'
Requires-Dist: myst-nb>=1.1; extra == 'docs'
Requires-Dist: pydata-sphinx-theme>=0.15; extra == 'docs'
Requires-Dist: sphinx-copybutton; extra == 'docs'
Requires-Dist: sphinx-design; extra == 'docs'
Requires-Dist: sphinx-togglebutton; extra == 'docs'
Requires-Dist: sphinx>=7.3; extra == 'docs'
Provides-Extra: explore
Requires-Dist: folium; extra == 'explore'
Requires-Dist: mapclassify; extra == 'explore'
Provides-Extra: rioxarray
Requires-Dist: rioxarray; extra == 'rioxarray'
Description-Content-Type: text/markdown

# entityraster

**Raster-valued attributes for GeoPandas: one raster per row, with explicit zero-vs-unknown semantics.**

[Documentation](https://entityraster.readthedocs.io) ·
[Getting started](https://entityraster.readthedocs.io/en/latest/getting_started.html) ·
[API reference](https://entityraster.readthedocs.io/en/latest/api/index.html) ·
[Changelog](https://entityraster.readthedocs.io/en/latest/changelog.html)

Many analyses attach a raster to an *entity*: a person's activity space, a patient's exposure surface, a site's
viability map, a parcel's time series. Usually these live as a file-path column plus hand-written loops (open, read,
align, accumulate, merge back), and the loops are where silent errors live: stacking rasters with different origins,
treating outside-extent cells as zero or as nodata without noticing, mismatched resolutions or CRS.

`entityraster` makes the raster a column. A **GeoRasterFrame** is an ordinary (Geo)DataFrame in which one column has the
pandas dtype `raster`: one lazy reference per row to a GeoTIFF/COG, with its header metadata cached. Pixels stay in the
files and are read only for the rows an operation touches. Filtering, `groupby`, `merge`, `concat`, `sort_values`,
`to_crs`, `sjoin` and `dissolve` work unchanged, and the table saves to GeoParquet with its references.

```python
gdf["area"] = gdf["activity_space"].raster.area(threshold=0.5, relative=True)   # one value per row
old = gdf[gdf["age"] > 60]                                                      # rasters follow the filter
surfaces = old.groupby("gender")["activity_space"].agg(er.mean_surface(outside="zero"))
```

> **Status:** alpha. `entityraster` is a research prototype from
> [Urban GeoInformatics](https://github.com/Urban-GeoInformatics), evaluated on synthetic data; a software paper is
> in preparation. The API may still change.

## Installation

```bash
pip install entityraster
pip install "entityraster[explore]"      # + folium / mapclassify for GeoDataFrame.explore()
```

Requires Python 3.11 or newer. Tested on Python 3.11–3.13 (Linux, macOS, Windows) and against the oldest supported
versions of the dependencies (pandas 2.0, GeoPandas 1.0, shapely 2.0, rasterio 1.3, numpy 1.26).

## Quick example

```python
import entityraster as er
from entityraster import synth

data = synth.make_dataset("data", n=1000, seed=42)   # a synthetic city; for your own files see er.embed below
gdf = data.people                                     # GeoDataFrame with an `activity_space` raster column

gdf["area"] = gdf["activity_space"].raster.area(threshold=0.5, relative=True)   # one value per person -> a column
old = gdf[gdf["age"] > 60]                                                      # rasters follow the filter
cube = old.rst.surface("activity_space", by="gender", outside="unknown")        # mean surface per group (xarray)
cube.sel(gender="female").plot()
```

Attach your own rasters by key, never by position:

```python
gdf = er.embed(people, catalogue, on="person_id", path_col="file")             # join a catalogue of files
gdf = er.embed(people, template="rasters/p{person_id:05d}.tif")                 # or a file-name template
```

## Five things to know

1. **Attach rasters:** `er.embed(table, rasters, on=…, pattern=… | template=…, column=…, index=…, geometry=…)`.
   Everything else has a safe default; problems (missing files, duplicate keys, orphan rasters) are reported, never silent.
2. **One entity:** `gdf.loc[uid, "col"]` is that entity's raster: `.mean()`, `.area()`, `.plot()`, `.values()`,
   `.to_xarray()`. A missing raster is a missing value like any other (`.mean()` is `nan`, `pd.isna(...)` is `True`).
3. **Every entity:** `gdf["col"].raster.mean()` / `.area()` / `.exposure(layer, outside=…)` / `.composition(landuse, outside=…)`
   return a Series or DataFrame aligned to the table's index.
4. **Across entities:** `gdf.rst.surface("col", by="group", outside=…)`, `stack`, `overlap`. Here you must say what cells
   outside an entity's raster mean (see below).
5. **Pixels are lazy:** the table stores references, so the files must stay where they were when the table was built.
   `er.to_geoparquet` / `er.read_geoparquet` save and restore the table; `er.to_geotiff` exports any surface for QGIS/ArcGIS.

## Zero or unknown?

Every cross-entity operation **requires** `outside="zero"` or `outside="unknown"` (alias `"nodata"`); leaving it out
raises `TypeError`, because the choice changes the numbers.

| | `outside="zero"` | `outside="unknown"` |
|---|---|---|
| cells outside an entity's raster (or nodata) | known to be 0: never there | unobserved: unknown |
| mean surface | divides by **all** entities | divides by entities that **observed** the cell; NaN where nobody did |
| `exposure(env)` | missing env cells count as 0 | dropped from numerator **and** denominator |
| `overlap` | non-intersecting pairs are exactly 0 | non-intersecting pairs are NaN |

Rasters with different CRS, resolution or grid origin are an error that names the offending rows, unless you opt in
with `reproject=True`, `resampling=...` or `snap=True`.
See [Concepts](https://entityraster.readthedocs.io/en/latest/user_guide/concepts.html) and
[Defaults](https://entityraster.readthedocs.io/en/latest/user_guide/defaults.html).

## When to use something else

`entityraster` is built for *one raster per entity, with different extents, kept with the entity's attributes*. For
one shared raster over many polygons, use rasterstats or exactextract; for regular aligned cubes, xarray/rioxarray; for
SQL-engine scale, SedonaDB, Apache Sedona or PostGIS raster. (It is unrelated to the Spark-based LocationTech
*RasterFrames*.) See [How it compares](https://entityraster.readthedocs.io/en/latest/comparison.html) for a measured
comparison with rasterio loops and xarray.

## Limitations

* Evaluated on synthetic data; not yet benchmarked on large real collections.
* North-up, unrotated GeoTIFFs only; one band per reference.
* Single process: no parallelism or out-of-core scheduling. `stack` materializes `n_entities × grid`; surfaces stream.
* References store absolute paths; moving the files breaks them.
* `zero` and `unknown` are two bookends; partial or probabilistic observability is not modelled.
* Plain `gpd.read_parquet` returns the raster column as dicts; use `er.read_geoparquet` to restore it.

## Development

```bash
git clone https://github.com/Urban-GeoInformatics/entityraster
cd entityraster
uv sync && uv run pytest
uv run python experiments/run_all.py     # regenerate the experiments of the paper
```

See [CONTRIBUTING.md](https://github.com/Urban-GeoInformatics/entityraster/blob/main/CONTRIBUTING.md).

## Citing and license

If you use `entityraster` in research, please cite it; see
[Citing](https://entityraster.readthedocs.io/en/latest/citing.html) or
[CITATION.cff](https://github.com/Urban-GeoInformatics/entityraster/blob/main/CITATION.cff).
BSD-3-Clause licensed.
