spatial-polars 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- spatial_polars/__init__.py +13 -0
- spatial_polars/_utils.py +162 -0
- spatial_polars/io.py +459 -0
- spatial_polars/spatialexpr.py +2017 -0
- spatial_polars/spatialframe.py +1552 -0
- spatial_polars/spatialseries.py +1072 -0
- spatial_polars/vendor.py +32 -0
- spatial_polars-0.1.0.dist-info/METADATA +54 -0
- spatial_polars-0.1.0.dist-info/RECORD +12 -0
- spatial_polars-0.1.0.dist-info/WHEEL +4 -0
- spatial_polars-0.1.0.dist-info/entry_points.txt +2 -0
- spatial_polars-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from .io import spatial_series_dtype, scan_spatial, read_spatial
|
|
2
|
+
from .spatialframe import SpatialFrame
|
|
3
|
+
from .spatialseries import SpatialSeries
|
|
4
|
+
from .spatialexpr import SpatialExpr
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"spatial_series_dtype",
|
|
8
|
+
"scan_spatial",
|
|
9
|
+
"read_spatial",
|
|
10
|
+
"SpatialFrame",
|
|
11
|
+
"SpatialSeries",
|
|
12
|
+
"SpatialExpr",
|
|
13
|
+
]
|
spatial_polars/_utils.py
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import polars as pl
|
|
7
|
+
from palettable.palette import Palette
|
|
8
|
+
from polars import selectors as cs
|
|
9
|
+
from matplotlib.colors import Colormap
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def validate_col_exists_in_df(df: pl.DataFrame, col: Any):
|
|
13
|
+
if col not in df.columns:
|
|
14
|
+
# verify column exists in df
|
|
15
|
+
raise ValueError(
|
|
16
|
+
f"Input `{col=}` does not exist in frame. Please provide the name of a column from the dataframe ({df.columns})."
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def validate_col_is_numeric(df: pl.DataFrame, col: Any):
|
|
21
|
+
if df[col].dtype.is_numeric() is False:
|
|
22
|
+
# verify cmap_col is a numeric dtype.
|
|
23
|
+
raise ValueError(
|
|
24
|
+
f"The '{col}' column is not a numeric column. Please provide a numeric column."
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def validate_cmap_input(
|
|
29
|
+
df: pl.DataFrame,
|
|
30
|
+
cmap_col: Any,
|
|
31
|
+
cmap_type: Any,
|
|
32
|
+
cmap: Any,
|
|
33
|
+
alpha: Any,
|
|
34
|
+
normalize_cmap_col: Any,
|
|
35
|
+
):
|
|
36
|
+
"""
|
|
37
|
+
Validates the input combinations for color mapping options.
|
|
38
|
+
"""
|
|
39
|
+
if cmap_col is None:
|
|
40
|
+
# no column provided is always good, use lonboard default color
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
validate_col_exists_in_df(df, cmap_col)
|
|
44
|
+
|
|
45
|
+
if cmap_type not in ["categorical", "continuous"]:
|
|
46
|
+
# verify cmap type is expected
|
|
47
|
+
raise ValueError(
|
|
48
|
+
f"`cmap_col` was provided, but {cmap_type=}. `c_map` must be `categorical` or `continuous` when `cmap_col` is provided."
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
if cmap_type == "continuous":
|
|
52
|
+
if isinstance(cmap, (Palette, Colormap)) is False:
|
|
53
|
+
# verify continuous cmaps get a cmap or appropriate type.
|
|
54
|
+
raise ValueError(
|
|
55
|
+
f"`{cmap_type=}` is `continuous`, but {cmap=}. Please provide a palettable.palette.Palette or matplotlib.colors.Colormap when using `cmap_type`=`continuous`."
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
validate_col_is_numeric(df, cmap_col)
|
|
59
|
+
|
|
60
|
+
if cmap_type == "categorical":
|
|
61
|
+
if isinstance(cmap, dict | None) is False:
|
|
62
|
+
# verify categorical cmaps get a cmap or appropriate type.
|
|
63
|
+
raise ValueError(
|
|
64
|
+
"`cmap_type` is `categorical`, but `cmap` is not a dictionary or None. Please provide a dictionary or None when using `cmap_type`=`categorical`."
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
if isinstance(cmap, dict):
|
|
68
|
+
# verify values in dataframe column all exist in cmap dictionary keys
|
|
69
|
+
try:
|
|
70
|
+
cmap_keys_s = pl.Series("cmap_keys", list(cmap.keys()))
|
|
71
|
+
except Exception:
|
|
72
|
+
raise ValueError(
|
|
73
|
+
"`cmap_type` is `categorical` and `cmap` is a dictionary, but there was an error converting the dictionary keys to a polars series. Please provide a dictionary with keys that are all the appropriate datatype for the data in the column."
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
col_vals_and_keys_df = (
|
|
77
|
+
df[cmap_col]
|
|
78
|
+
.unique()
|
|
79
|
+
.to_frame()
|
|
80
|
+
.join(
|
|
81
|
+
cmap_keys_s.to_frame().with_columns(
|
|
82
|
+
pl.col("cmap_keys").cast(df[cmap_col].dtype),
|
|
83
|
+
pl.lit(True).alias("exists_in_keys"),
|
|
84
|
+
),
|
|
85
|
+
how="left",
|
|
86
|
+
left_on=cmap_col,
|
|
87
|
+
right_on="cmap_keys",
|
|
88
|
+
)
|
|
89
|
+
)
|
|
90
|
+
if col_vals_and_keys_df["exists_in_keys"].is_null().any():
|
|
91
|
+
missing_values = col_vals_and_keys_df.filter(
|
|
92
|
+
pl.col("exists_in_keys").is_null()
|
|
93
|
+
)[cmap_col].to_list()
|
|
94
|
+
raise ValueError(
|
|
95
|
+
f"`cmap` dictionary keys do not include all values in the data. Please provide colors for the following values {missing_values}."
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# verify values have three or four items
|
|
99
|
+
try:
|
|
100
|
+
cmap_vals_df = pl.DataFrame(list(cmap.values()), orient="row")
|
|
101
|
+
except Exception:
|
|
102
|
+
raise ValueError(
|
|
103
|
+
"`cmap` dictionary values could not be converted to a dataframe with three or four integer columns for color codes. Please provide RGB[A] color codes for all values."
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
col_count = cmap_vals_df.shape[1]
|
|
107
|
+
if col_count not in [3, 4]:
|
|
108
|
+
raise ValueError(
|
|
109
|
+
"`cmap` dictionary values could not be converted to a dataframe with three or four integer columns for color codes. Please provide RGB[A] color codes for all values."
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
if col_count == 3:
|
|
113
|
+
cmap_vals_df.columns = ["R", "G", "B"]
|
|
114
|
+
elif col_count == 4:
|
|
115
|
+
cmap_vals_df.columns = ["R", "G", "B", "A"]
|
|
116
|
+
|
|
117
|
+
# verify all RGB[A] values are between 0-255
|
|
118
|
+
oob_rgba_df = cmap_vals_df.select(cmap_keys_s, pl.all()).filter(
|
|
119
|
+
pl.any_horizontal(
|
|
120
|
+
~cs.by_name("R", "G", "B", "A", require_all=False).is_between(
|
|
121
|
+
0, 255, closed="both"
|
|
122
|
+
)
|
|
123
|
+
)
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
if len(oob_rgba_df) > 0:
|
|
127
|
+
raise ValueError(
|
|
128
|
+
f"`cmap` dictionary values for color codes were not all between 0 and 255. Please provide number between 0-255 for all RGB[A] color codes. Bad values were: {oob_rgba_df}"
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def validate_width_and_radius_input(df: pl.DataFrame, width: Any):
|
|
133
|
+
if width is None:
|
|
134
|
+
# None is always good
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
elif isinstance(width, float | int):
|
|
138
|
+
# numbers are always good
|
|
139
|
+
return
|
|
140
|
+
elif isinstance(width, np.ndarray):
|
|
141
|
+
if np.issubdtype(width.dtype, np.number) is False:
|
|
142
|
+
# validate numpy array dtype is numeric
|
|
143
|
+
raise ValueError(
|
|
144
|
+
"Input `width` is a numpy array that is not numeric. If using a numpy array for the width, the datatype of the array must be numeric."
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
width_len = width.shape[0]
|
|
148
|
+
df_len = len(df)
|
|
149
|
+
if width_len != df_len:
|
|
150
|
+
# validate numpy array has the same number of items as the df
|
|
151
|
+
raise ValueError(
|
|
152
|
+
f"Input `width` is a numpy array with a different number of elements than the frame (array: {width_len:,} frame: {df_len:,}). If using a numpy array for the width, the the array must have the same number of elements as the frame has rows."
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
elif isinstance(width, str):
|
|
156
|
+
validate_col_exists_in_df(df, width)
|
|
157
|
+
validate_col_is_numeric(df, width)
|
|
158
|
+
|
|
159
|
+
else:
|
|
160
|
+
raise Exception(
|
|
161
|
+
f"Input `width` '{width}' is a '{type(width)}', which is not allowed. Please provide: None, a number, a column name, or a numpy array for the width."
|
|
162
|
+
)
|
spatial_polars/io.py
ADDED
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Iterator, Optional, TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
import polars as pl
|
|
6
|
+
import pyogrio
|
|
7
|
+
import pyproj
|
|
8
|
+
import shapely
|
|
9
|
+
import pyarrow.parquet as _pq
|
|
10
|
+
import json
|
|
11
|
+
|
|
12
|
+
from polars.io.plugins import register_io_source
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from io import BytesIO
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"spatial_series_dtype",
|
|
19
|
+
"scan_spatial",
|
|
20
|
+
"read_spatial",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
spatial_series_dtype = pl.Struct({"wkb_geometry": pl.Binary, "crs": pl.Categorical})
|
|
24
|
+
|
|
25
|
+
PYOGRIO_POLARS_DTYPES = {
|
|
26
|
+
"int8": pl.Int8,
|
|
27
|
+
"int16": pl.Int16,
|
|
28
|
+
"int32": pl.Int32,
|
|
29
|
+
"int": pl.Int64,
|
|
30
|
+
"int64": pl.Int64,
|
|
31
|
+
"uint8": pl.UInt8,
|
|
32
|
+
"uint16": pl.UInt16,
|
|
33
|
+
"uint32": pl.UInt32,
|
|
34
|
+
"uint": pl.UInt64,
|
|
35
|
+
"uint64": pl.UInt64,
|
|
36
|
+
"bool": pl.Boolean,
|
|
37
|
+
"float32": pl.Float32,
|
|
38
|
+
"float": pl.Float64,
|
|
39
|
+
"float64": pl.Float64,
|
|
40
|
+
"datetime64[D]": pl.Date,
|
|
41
|
+
"datetime64[us]": pl.Datetime("us"),
|
|
42
|
+
"datetime64[ns]": pl.Datetime("ns"),
|
|
43
|
+
"datetime64[ms]": pl.Datetime("ms"),
|
|
44
|
+
"datetime64": pl.Datetime("ms"), # ms??
|
|
45
|
+
"object": pl.String,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def scan_spatial(
|
|
50
|
+
path_or_buffer: str | Path | BytesIO,
|
|
51
|
+
layer: Optional[str | int] = None,
|
|
52
|
+
encoding: Optional[str] = None,
|
|
53
|
+
bbox: Optional[tuple[float, float, float, float]] = None,
|
|
54
|
+
mask: Optional[shapely.Polygon] = None,
|
|
55
|
+
) -> pl.LazyFrame:
|
|
56
|
+
r"""
|
|
57
|
+
Scans a data source [supported by pyogrio](https://pyogrio.readthedocs.io/en/stable/supported_formats.html) or a geoparquet file to produce a polars LazyFrame.
|
|
58
|
+
|
|
59
|
+
Note
|
|
60
|
+
----
|
|
61
|
+
Although geoparquet is supported, this implementation, in its current state, leaves a lot to be desired.
|
|
62
|
+
|
|
63
|
+
Parameters
|
|
64
|
+
----------
|
|
65
|
+
|
|
66
|
+
path_or_buffer
|
|
67
|
+
A dataset path or URI, raw buffer, or file-like object with a read method.
|
|
68
|
+
|
|
69
|
+
layer
|
|
70
|
+
If an integer is provided, it corresponds to the index of the layer with the
|
|
71
|
+
data source. If a string is provided, it must match the name of the layer in
|
|
72
|
+
the data source. Defaults to first layer in data source.
|
|
73
|
+
|
|
74
|
+
encoding
|
|
75
|
+
If present, will be used as the encoding for reading string values from the
|
|
76
|
+
data source. By default will automatically try to detect the native encoding
|
|
77
|
+
and decode to UTF-8.
|
|
78
|
+
|
|
79
|
+
bbox
|
|
80
|
+
If present, will be used to filter records whose geometry intersects this
|
|
81
|
+
box. This must be in the same CRS as the dataset. If GEOS is present and
|
|
82
|
+
used by GDAL, only geometries that intersect this bbox will be returned;
|
|
83
|
+
if GEOS is not available or not used by GDAL, all geometries with bounding
|
|
84
|
+
boxes that intersect this bbox will be returned. Cannot be combined with mask
|
|
85
|
+
keyword. Tuple should be in the format of (xmin, ymin, xmax, ymax).
|
|
86
|
+
|
|
87
|
+
mask
|
|
88
|
+
If present, will be used to filter records whose geometry intersects this
|
|
89
|
+
geometry. This must be in the same CRS as the dataset. If GEOS is present
|
|
90
|
+
and used by GDAL, only geometries that intersect this geometry will be
|
|
91
|
+
returned; if GEOS is not available or not used by GDAL, all geometries with
|
|
92
|
+
bounding boxes that intersect the bounding box of this geometry will be
|
|
93
|
+
returned. Requires Shapely >= 2.0. Cannot be combined with bbox keyword.
|
|
94
|
+
|
|
95
|
+
Examples
|
|
96
|
+
--------
|
|
97
|
+
**Scanning a layer from a geopackage:**
|
|
98
|
+
|
|
99
|
+
>>> my_geopackage = r"c:\data\hiking_club.gpkg"
|
|
100
|
+
>>> lf = scan_spatial(my_geopackage, layer="hike")
|
|
101
|
+
>>> lf
|
|
102
|
+
naive plan: (run LazyFrame.explain(optimized=True) to see the optimized plan)
|
|
103
|
+
PYTHON SCAN []
|
|
104
|
+
PROJECT */4 COLUMNS
|
|
105
|
+
|
|
106
|
+
**Scanning a shapefile:**
|
|
107
|
+
|
|
108
|
+
>>> from spatial_polars import scan_spatial
|
|
109
|
+
>>> my_shapefile = r"c:\data\roads.shp"
|
|
110
|
+
>>> lf = scan_spatial(my_shapefile)
|
|
111
|
+
>>> lf
|
|
112
|
+
naive plan: (run LazyFrame.explain(optimized=True) to see the optimized plan)
|
|
113
|
+
PYTHON SCAN []
|
|
114
|
+
PROJECT */11 COLUMNS
|
|
115
|
+
|
|
116
|
+
**Scanning a shapefile from within a zipped directory:**
|
|
117
|
+
|
|
118
|
+
>>> zipped_shapefiles = r"C:\data\illinois-latest-free.shp.zip"
|
|
119
|
+
>>> lf = scan_spatial(zipped_shapefiles, layer="gis_osm_roads_free_1")
|
|
120
|
+
>>> lf
|
|
121
|
+
naive plan: (run LazyFrame.explain(optimized=True) to see the optimized plan)
|
|
122
|
+
PYTHON SCAN []
|
|
123
|
+
PROJECT */11 COLUMNS
|
|
124
|
+
"""
|
|
125
|
+
if isinstance(path_or_buffer, (str, Path)) and str(path_or_buffer).endswith(
|
|
126
|
+
".parquet"
|
|
127
|
+
):
|
|
128
|
+
# TODO look into libgdal-arrow-parquet from conda forge
|
|
129
|
+
# https://pyogrio.readthedocs.io/en/latest/install.html#conda-forge
|
|
130
|
+
schema = pl.scan_parquet(path_or_buffer).collect_schema()
|
|
131
|
+
if schema.get("geometry") is not None:
|
|
132
|
+
schema["geometry"] = spatial_series_dtype
|
|
133
|
+
if bbox is not None:
|
|
134
|
+
mask = shapely.Polygon(shapely.box(*bbox))
|
|
135
|
+
|
|
136
|
+
def source_generator(
|
|
137
|
+
with_columns: list[str] | None,
|
|
138
|
+
predicate: pl.Expr | None,
|
|
139
|
+
n_rows: int | None,
|
|
140
|
+
batch_size: int | None,
|
|
141
|
+
) -> Iterator[pl.DataFrame]:
|
|
142
|
+
"""
|
|
143
|
+
Generator function that creates the source.
|
|
144
|
+
This function will be registered as IO source.
|
|
145
|
+
"""
|
|
146
|
+
if mask is not None:
|
|
147
|
+
if "geometry" not in with_columns:
|
|
148
|
+
with_columns.append("geometry")
|
|
149
|
+
|
|
150
|
+
tab = _pq.read_table(path_or_buffer)
|
|
151
|
+
tab_metadata = tab.schema.metadata if tab.schema.metadata else {}
|
|
152
|
+
if b"geo" in tab_metadata:
|
|
153
|
+
geo_meta = json.loads(tab_metadata[b"geo"])
|
|
154
|
+
else:
|
|
155
|
+
geo_meta = {}
|
|
156
|
+
geom_col = geo_meta["primary_column"]
|
|
157
|
+
crs_wkt = pyproj.CRS(geo_meta["columns"][geom_col]["crs"]).to_wkt()
|
|
158
|
+
|
|
159
|
+
if batch_size is None:
|
|
160
|
+
batch_size = 10000
|
|
161
|
+
|
|
162
|
+
if with_columns is None:
|
|
163
|
+
read_geometry = True
|
|
164
|
+
elif "geometry" in with_columns:
|
|
165
|
+
read_geometry = True
|
|
166
|
+
else:
|
|
167
|
+
read_geometry = False
|
|
168
|
+
|
|
169
|
+
lf = pl.scan_parquet(path_or_buffer)
|
|
170
|
+
|
|
171
|
+
if with_columns is not None:
|
|
172
|
+
lf = lf.select(with_columns)
|
|
173
|
+
|
|
174
|
+
if predicate is not None:
|
|
175
|
+
lf = lf.filter(predicate)
|
|
176
|
+
|
|
177
|
+
previous_max = 0
|
|
178
|
+
while n_rows is None or n_rows > 0:
|
|
179
|
+
batch = lf.slice(previous_max, previous_max + batch_size).collect()
|
|
180
|
+
if batch.height is None or batch.height == 0:
|
|
181
|
+
break
|
|
182
|
+
if n_rows is not None and n_rows <= 0:
|
|
183
|
+
break
|
|
184
|
+
|
|
185
|
+
if read_geometry:
|
|
186
|
+
# get the geometries from the batch
|
|
187
|
+
geometries = batch[0:n_rows][geom_col]
|
|
188
|
+
shapely_goms = shapely.from_wkb(geometries)
|
|
189
|
+
geometries = shapely.to_wkb(shapely_goms)
|
|
190
|
+
|
|
191
|
+
# create the dataframe with the non geometry columns
|
|
192
|
+
# then add struct column with the WKB geometries/CRS
|
|
193
|
+
df = pl.DataFrame(batch[0:n_rows].drop(geom_col)).with_columns(
|
|
194
|
+
pl.struct(
|
|
195
|
+
pl.Series("wkb_geometry", geometries, dtype=pl.Binary),
|
|
196
|
+
pl.lit(crs_wkt, dtype=pl.Categorical).alias("crs"),
|
|
197
|
+
).alias("geometry")
|
|
198
|
+
)
|
|
199
|
+
else:
|
|
200
|
+
df = pl.DataFrame(batch[0:n_rows])
|
|
201
|
+
previous_max += df.height
|
|
202
|
+
|
|
203
|
+
if n_rows is not None:
|
|
204
|
+
n_rows -= df.height
|
|
205
|
+
|
|
206
|
+
if predicate is not None:
|
|
207
|
+
df = df.filter(predicate)
|
|
208
|
+
|
|
209
|
+
if mask is not None:
|
|
210
|
+
df = df.filter(pl.col("geometry").spatial.intersects(mask))
|
|
211
|
+
if mask is not None:
|
|
212
|
+
if "geometry" not in with_columns:
|
|
213
|
+
df = df.drop("geometry")
|
|
214
|
+
|
|
215
|
+
yield df
|
|
216
|
+
|
|
217
|
+
else:
|
|
218
|
+
# not geoparquet
|
|
219
|
+
layer_info = pyogrio.read_info(path_or_buffer, layer=layer, encoding=encoding)
|
|
220
|
+
schema = dict(
|
|
221
|
+
zip(
|
|
222
|
+
layer_info["fields"],
|
|
223
|
+
[PYOGRIO_POLARS_DTYPES[dt] for dt in layer_info["dtypes"]],
|
|
224
|
+
)
|
|
225
|
+
)
|
|
226
|
+
if layer_info.get("geometry_type"):
|
|
227
|
+
schema["geometry"] = spatial_series_dtype
|
|
228
|
+
|
|
229
|
+
def source_generator(
|
|
230
|
+
with_columns: list[str] | None,
|
|
231
|
+
predicate: pl.Expr | None,
|
|
232
|
+
n_rows: int | None,
|
|
233
|
+
batch_size: int | None,
|
|
234
|
+
) -> Iterator[pl.DataFrame]:
|
|
235
|
+
"""
|
|
236
|
+
Generator function that creates the source.
|
|
237
|
+
This function will be registered as IO source.
|
|
238
|
+
"""
|
|
239
|
+
|
|
240
|
+
if batch_size is None:
|
|
241
|
+
batch_size = 100
|
|
242
|
+
|
|
243
|
+
if with_columns is None:
|
|
244
|
+
read_geometry = True
|
|
245
|
+
elif "geometry" in with_columns:
|
|
246
|
+
read_geometry = True
|
|
247
|
+
with_columns.remove("geometry")
|
|
248
|
+
else:
|
|
249
|
+
read_geometry = False
|
|
250
|
+
|
|
251
|
+
with pyogrio.open_arrow(
|
|
252
|
+
path_or_buffer,
|
|
253
|
+
layer=layer,
|
|
254
|
+
encoding=encoding,
|
|
255
|
+
columns=with_columns,
|
|
256
|
+
read_geometry=read_geometry,
|
|
257
|
+
force_2d=False,
|
|
258
|
+
bbox=bbox,
|
|
259
|
+
mask=mask,
|
|
260
|
+
batch_size=batch_size,
|
|
261
|
+
use_pyarrow=True,
|
|
262
|
+
) as source:
|
|
263
|
+
meta, reader = source
|
|
264
|
+
|
|
265
|
+
# extract the crs from the metadata
|
|
266
|
+
crs_wkt = pyproj.CRS(meta["crs"]).to_wkt()
|
|
267
|
+
|
|
268
|
+
geom_col = meta["geometry_name"] or "wkb_geometry"
|
|
269
|
+
|
|
270
|
+
while n_rows is None or n_rows > 0:
|
|
271
|
+
for batch in reader:
|
|
272
|
+
if n_rows is not None and n_rows <= 0:
|
|
273
|
+
break
|
|
274
|
+
|
|
275
|
+
if read_geometry:
|
|
276
|
+
# get the geometries from the batch
|
|
277
|
+
geometries = batch[geom_col][0:n_rows]
|
|
278
|
+
shapely_goms = shapely.from_wkb(geometries)
|
|
279
|
+
geometries = shapely.to_wkb(shapely_goms)
|
|
280
|
+
|
|
281
|
+
print(batch)
|
|
282
|
+
|
|
283
|
+
# create the dataframe with the non geometry columns
|
|
284
|
+
# then add struct column with the WKB geometries/CRS
|
|
285
|
+
df = pl.DataFrame(
|
|
286
|
+
batch[0:n_rows].drop_columns(geom_col)
|
|
287
|
+
).with_columns(
|
|
288
|
+
pl.struct(
|
|
289
|
+
pl.Series(
|
|
290
|
+
"wkb_geometry", geometries, dtype=pl.Binary
|
|
291
|
+
),
|
|
292
|
+
pl.lit(crs_wkt, dtype=pl.Categorical).alias("crs"),
|
|
293
|
+
).alias("geometry")
|
|
294
|
+
)
|
|
295
|
+
else:
|
|
296
|
+
df = pl.DataFrame(batch[0:n_rows])
|
|
297
|
+
|
|
298
|
+
if n_rows is not None:
|
|
299
|
+
n_rows -= df.height
|
|
300
|
+
|
|
301
|
+
if predicate is not None:
|
|
302
|
+
df = df.filter(predicate)
|
|
303
|
+
|
|
304
|
+
yield df
|
|
305
|
+
if n_rows is None or n_rows <= 0:
|
|
306
|
+
break
|
|
307
|
+
|
|
308
|
+
return register_io_source(io_source=source_generator, schema=schema)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def read_spatial(
|
|
312
|
+
path_or_buffer: str | Path | BytesIO,
|
|
313
|
+
layer: Optional[str | int] = None,
|
|
314
|
+
encoding: Optional[str] = None,
|
|
315
|
+
bbox: Optional[tuple[float, float, float, float]] = None,
|
|
316
|
+
mask: Optional[shapely.Polygon] = None,
|
|
317
|
+
) -> pl.DataFrame:
|
|
318
|
+
r"""
|
|
319
|
+
Reads a spatial data source [supported by pyogrio](https://pyogrio.readthedocs.io/en/stable/supported_formats.html) to produce a polars DataFrame.
|
|
320
|
+
|
|
321
|
+
Note
|
|
322
|
+
----
|
|
323
|
+
Although geoparquet is supported, this implementation, in its current state, leaves a lot to be desired.
|
|
324
|
+
|
|
325
|
+
Parameters
|
|
326
|
+
----------
|
|
327
|
+
|
|
328
|
+
path_or_buffer
|
|
329
|
+
A dataset path or URI, raw buffer, or file-like object with a read method.
|
|
330
|
+
|
|
331
|
+
layer
|
|
332
|
+
If an integer is provided, it corresponds to the index of the layer with the
|
|
333
|
+
data source. If a string is provided, it must match the name of the layer in
|
|
334
|
+
the data source. Defaults to first layer in data source.
|
|
335
|
+
|
|
336
|
+
encoding
|
|
337
|
+
If present, will be used as the encoding for reading string values from the
|
|
338
|
+
data source. By default will automatically try to detect the native encoding
|
|
339
|
+
and decode to UTF-8.
|
|
340
|
+
|
|
341
|
+
bbox
|
|
342
|
+
If present, will be used to filter records whose geometry intersects this
|
|
343
|
+
box. This must be in the same CRS as the dataset. If GEOS is present and
|
|
344
|
+
used by GDAL, only geometries that intersect this bbox will be returned;
|
|
345
|
+
if GEOS is not available or not used by GDAL, all geometries with bounding
|
|
346
|
+
boxes that intersect this bbox will be returned. Cannot be combined with mask
|
|
347
|
+
keyword. Tuple should be in the format of (xmin, ymin, xmax, ymax).
|
|
348
|
+
|
|
349
|
+
mask
|
|
350
|
+
If present, will be used to filter records whose geometry intersects this
|
|
351
|
+
geometry. This must be in the same CRS as the dataset. If GEOS is present
|
|
352
|
+
and used by GDAL, only geometries that intersect this geometry will be
|
|
353
|
+
returned; if GEOS is not available or not used by GDAL, all geometries with
|
|
354
|
+
bounding boxes that intersect the bounding box of this geometry will be
|
|
355
|
+
returned. Requires Shapely >= 2.0. Cannot be combined with bbox keyword.
|
|
356
|
+
|
|
357
|
+
Examples
|
|
358
|
+
--------
|
|
359
|
+
**Scanning a layer from a geopackage:**
|
|
360
|
+
|
|
361
|
+
>>> from spatial_polars import read_spatial
|
|
362
|
+
>>> my_geopackage = r"c:\data\hiking_club.gpkg"
|
|
363
|
+
>>> df = read_spatial(my_geopackage, layer="hike")
|
|
364
|
+
>>> df
|
|
365
|
+
shape: (31, 4)
|
|
366
|
+
┌─────────────────────────────────┬────────────┬──────────┬─────────────────────────────────┐
|
|
367
|
+
│ LOCATION ┆ DATE ┆ DISTANCE ┆ geometry │
|
|
368
|
+
│ --- ┆ --- ┆ --- ┆ --- │
|
|
369
|
+
│ str ┆ date ┆ f64 ┆ struct[2] │
|
|
370
|
+
╞═════════════════════════════════╪════════════╪══════════╪═════════════════════════════════╡
|
|
371
|
+
│ Watershed Nature Center ┆ 2023-01-14 ┆ 1.25 ┆ {b"\x01\x02\x00\x00\x00\xd8\x0… │
|
|
372
|
+
│ Ellis Island ┆ 2023-03-11 ┆ 2.25 ┆ {b"\x01\x02\x00\x00\x00\x82\x0… │
|
|
373
|
+
│ Cahokia Mounds State Historic … ┆ 2023-02-04 ┆ 1.75 ┆ {b"\x01\x02\x00\x00\x00\xb1\x0… │
|
|
374
|
+
│ Willoughby Heritage Farm ┆ 2022-12-03 ┆ 0.75 ┆ {b"\x01\x02\x00\x00\x00\xef\x0… │
|
|
375
|
+
│ Pere Marquette State Park ┆ 2022-10-15 ┆ 1.0 ┆ {b"\x01\x02\x00\x00\x002\x02\x… │
|
|
376
|
+
│ … ┆ … ┆ … ┆ … │
|
|
377
|
+
│ Haunted Glen Carbon ┆ 2024-10-19 ┆ 1.75 ┆ {b"\x01\x02\x00\x00\x00\x82\x0… │
|
|
378
|
+
│ Watershed Nature Center ┆ 2024-10-08 ┆ 1.0 ┆ {b"\x01\x02\x00\x00\x000\x00\x… │
|
|
379
|
+
│ Beaver Dam State Park ┆ 2024-10-26 ┆ 2.0 ┆ {b"\x01\x02\x00\x00\x00\xc4\x0… │
|
|
380
|
+
│ Willoughby Heritage Farm ┆ 2024-12-07 ┆ 1.5 ┆ {b"\x01\x02\x00\x00\x00>\x02\x… │
|
|
381
|
+
│ Cahokia Mounds State Historic … ┆ 2025-03-08 ┆ 1.75 ┆ {b"\x01\x02\x00\x00\x00\xeb\x0… │
|
|
382
|
+
└─────────────────────────────────┴────────────┴──────────┴─────────────────────────────────┘
|
|
383
|
+
|
|
384
|
+
**Scanning a shapefile:**
|
|
385
|
+
|
|
386
|
+
>>> my_shapefile = r"c:\data\roads.shp"
|
|
387
|
+
>>> df = read_spatial(my_shapefile)
|
|
388
|
+
>>> df
|
|
389
|
+
shape: (1_662_837, 11)
|
|
390
|
+
┌────────────┬──────┬─────────────┬─────────────┬───┬───────┬────────┬────────┬────────────────────┐
|
|
391
|
+
│ osm_id ┆ code ┆ fclass ┆ name ┆ … ┆ layer ┆ bridge ┆ tunnel ┆ geometry │
|
|
392
|
+
│ --- ┆ --- ┆ --- ┆ --- ┆ ┆ --- ┆ --- ┆ --- ┆ --- │
|
|
393
|
+
│ str ┆ i32 ┆ str ┆ str ┆ ┆ i64 ┆ str ┆ str ┆ struct[2] │
|
|
394
|
+
╞════════════╪══════╪═════════════╪═════════════╪═══╪═══════╪════════╪════════╪════════════════════╡
|
|
395
|
+
│ 4265057 ┆ 5114 ┆ secondary ┆ 55th Street ┆ … ┆ 0 ┆ F ┆ F ┆ {b"\x01\x02\x00\x0 │
|
|
396
|
+
│ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ 0\x00\x03\x0… │
|
|
397
|
+
│ 4265058 ┆ 5114 ┆ secondary ┆ Fairview ┆ … ┆ 0 ┆ F ┆ F ┆ {b"\x01\x02\x00\x0 │
|
|
398
|
+
│ ┆ ┆ ┆ Avenue ┆ ┆ ┆ ┆ ┆ 0\x00\x0e\x0… │
|
|
399
|
+
│ 4267607 ┆ 5114 ┆ secondary ┆ 31st Street ┆ … ┆ 0 ┆ F ┆ F ┆ {b"\x01\x02\x00\x0 │
|
|
400
|
+
│ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ 0\x00\x02\x0… │
|
|
401
|
+
│ 4271616 ┆ 5115 ┆ tertiary ┆ 59th Street ┆ … ┆ 0 ┆ F ┆ F ┆ {b"\x01\x02\x00\x0 │
|
|
402
|
+
│ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ 0\x00\x15\x0… │
|
|
403
|
+
│ 4275365 ┆ 5122 ┆ residential ┆ 61st Street ┆ … ┆ 0 ┆ F ┆ F ┆ {b"\x01\x02\x00\x0 │
|
|
404
|
+
│ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ 0\x00"\x00\x… │
|
|
405
|
+
│ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … │
|
|
406
|
+
│ 1370383592 ┆ 5153 ┆ footway ┆ null ┆ … ┆ 0 ┆ F ┆ F ┆ {b"\x01\x02\x00\x0 │
|
|
407
|
+
│ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ 0\x00\x02\x0… │
|
|
408
|
+
│ 1370383593 ┆ 5153 ┆ footway ┆ null ┆ … ┆ 0 ┆ F ┆ F ┆ {b"\x01\x02\x00\x0 │
|
|
409
|
+
│ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ 0\x00\x07\x0… │
|
|
410
|
+
│ 1370383594 ┆ 5153 ┆ footway ┆ null ┆ … ┆ 0 ┆ F ┆ F ┆ {b"\x01\x02\x00\x0 │
|
|
411
|
+
│ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ 0\x00\x1c\x0… │
|
|
412
|
+
│ 1370383595 ┆ 5153 ┆ footway ┆ null ┆ … ┆ 0 ┆ F ┆ F ┆ {b"\x01\x02\x00\x0 │
|
|
413
|
+
│ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ 0\x00\x0b\x0… │
|
|
414
|
+
│ 1370398885 ┆ 5141 ┆ service ┆ null ┆ … ┆ 0 ┆ F ┆ F ┆ {b"\x01\x02\x00\x0 │
|
|
415
|
+
│ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ 0\x00\x02\x0… │
|
|
416
|
+
└────────────┴──────┴─────────────┴─────────────┴───┴───────┴────────┴────────┴────────────────────┘
|
|
417
|
+
|
|
418
|
+
**Scanning a shapefile from within a zipped directory:**
|
|
419
|
+
|
|
420
|
+
>>> zipped_shapefiles = r"C:\data\illinois-latest-free.shp.zip"
|
|
421
|
+
>>> df = read_spatial(zipped_shapefiles, layer="gis_osm_roads_free_1")
|
|
422
|
+
>>> df
|
|
423
|
+
shape: (1_662_837, 11)
|
|
424
|
+
┌────────────┬──────┬─────────────┬─────────────┬───┬───────┬────────┬────────┬────────────────────┐
|
|
425
|
+
│ osm_id ┆ code ┆ fclass ┆ name ┆ … ┆ layer ┆ bridge ┆ tunnel ┆ geometry │
|
|
426
|
+
│ --- ┆ --- ┆ --- ┆ --- ┆ ┆ --- ┆ --- ┆ --- ┆ --- │
|
|
427
|
+
│ str ┆ i32 ┆ str ┆ str ┆ ┆ i64 ┆ str ┆ str ┆ struct[2] │
|
|
428
|
+
╞════════════╪══════╪═════════════╪═════════════╪═══╪═══════╪════════╪════════╪════════════════════╡
|
|
429
|
+
│ 4265057 ┆ 5114 ┆ secondary ┆ 55th Street ┆ … ┆ 0 ┆ F ┆ F ┆ {b"\x01\x02\x00\x0 │
|
|
430
|
+
│ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ 0\x00\x03\x0… │
|
|
431
|
+
│ 4265058 ┆ 5114 ┆ secondary ┆ Fairview ┆ … ┆ 0 ┆ F ┆ F ┆ {b"\x01\x02\x00\x0 │
|
|
432
|
+
│ ┆ ┆ ┆ Avenue ┆ ┆ ┆ ┆ ┆ 0\x00\x0e\x0… │
|
|
433
|
+
│ 4267607 ┆ 5114 ┆ secondary ┆ 31st Street ┆ … ┆ 0 ┆ F ┆ F ┆ {b"\x01\x02\x00\x0 │
|
|
434
|
+
│ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ 0\x00\x02\x0… │
|
|
435
|
+
│ 4271616 ┆ 5115 ┆ tertiary ┆ 59th Street ┆ … ┆ 0 ┆ F ┆ F ┆ {b"\x01\x02\x00\x0 │
|
|
436
|
+
│ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ 0\x00\x15\x0… │
|
|
437
|
+
│ 4275365 ┆ 5122 ┆ residential ┆ 61st Street ┆ … ┆ 0 ┆ F ┆ F ┆ {b"\x01\x02\x00\x0 │
|
|
438
|
+
│ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ 0\x00"\x00\x… │
|
|
439
|
+
│ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … │
|
|
440
|
+
│ 1370383592 ┆ 5153 ┆ footway ┆ null ┆ … ┆ 0 ┆ F ┆ F ┆ {b"\x01\x02\x00\x0 │
|
|
441
|
+
│ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ 0\x00\x02\x0… │
|
|
442
|
+
│ 1370383593 ┆ 5153 ┆ footway ┆ null ┆ … ┆ 0 ┆ F ┆ F ┆ {b"\x01\x02\x00\x0 │
|
|
443
|
+
│ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ 0\x00\x07\x0… │
|
|
444
|
+
│ 1370383594 ┆ 5153 ┆ footway ┆ null ┆ … ┆ 0 ┆ F ┆ F ┆ {b"\x01\x02\x00\x0 │
|
|
445
|
+
│ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ 0\x00\x1c\x0… │
|
|
446
|
+
│ 1370383595 ┆ 5153 ┆ footway ┆ null ┆ … ┆ 0 ┆ F ┆ F ┆ {b"\x01\x02\x00\x0 │
|
|
447
|
+
│ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ 0\x00\x0b\x0… │
|
|
448
|
+
│ 1370398885 ┆ 5141 ┆ service ┆ null ┆ … ┆ 0 ┆ F ┆ F ┆ {b"\x01\x02\x00\x0 │
|
|
449
|
+
│ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ 0\x00\x02\x0… │
|
|
450
|
+
└────────────┴──────┴─────────────┴─────────────┴───┴───────┴────────┴────────┴────────────────────┘
|
|
451
|
+
|
|
452
|
+
"""
|
|
453
|
+
return scan_spatial(
|
|
454
|
+
path_or_buffer=path_or_buffer,
|
|
455
|
+
layer=layer,
|
|
456
|
+
encoding=encoding,
|
|
457
|
+
bbox=bbox,
|
|
458
|
+
mask=mask,
|
|
459
|
+
).collect(engine="streaming")
|