You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sedona.apache.org by ji...@apache.org on 2026/08/26 03:50:10 UTC
(sedona) branch master updated: [GH-3281] GeoPandas: implement from_features (#3282)
This is an automated email from the ASF dual-hosted git repository.
jiayuasu pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/sedona.git
The following commit(s) were added to refs/heads/master by this push:
new 9b56c3be084 [GH-3281] GeoPandas: implement from_features (#3282)
9b56c3be084 is described below
commit 9b56c3be0841de8b254153a4a1fcfff73e05cb64
Author: Jia Yu <ji...@apache.org>
AuthorDate: Tue Aug 25 20:50:04 2026 -0700
[GH-3281] GeoPandas: implement from_features (#3282)
---
docs/setup/release-notes.md | 1 +
python/sedona/spark/geopandas/geodataframe.py | 98 ++++++++++++++++++++++++++-
python/tests/geopandas/test_geodataframe.py | 97 ++++++++++++++++++++++++++
3 files changed, 194 insertions(+), 2 deletions(-)
diff --git a/docs/setup/release-notes.md b/docs/setup/release-notes.md
index 9dc452e3a8e..50f07e4728e 100644
--- a/docs/setup/release-notes.md
+++ b/docs/setup/release-notes.md
@@ -50,6 +50,7 @@
* [<a href='https://github.com/apache/sedona/issues/3257'>GH-3257</a>] - Implement distributed GeoSeries and GeoDataFrame Hilbert-distance spatial ordering
* [<a href='https://github.com/apache/sedona/issues/3260'>GH-3260</a>] - Implement distributed GeoSeries and GeoDataFrame identical-geometry equality
* [<a href='https://github.com/apache/sedona/issues/3273'>GH-3273</a>] - Implement distributed GeoSeries and GeoDataFrame polygonal coverage validation and invalid-edge diagnostics
+* [<a href='https://github.com/apache/sedona/issues/3281'>GH-3281</a>] - Implement `GeoDataFrame.from_features` for in-memory GeoJSON-like features
### Bug Fixes
diff --git a/python/sedona/spark/geopandas/geodataframe.py b/python/sedona/spark/geopandas/geodataframe.py
index 9a0804d8ee5..f87f384ed89 100644
--- a/python/sedona/spark/geopandas/geodataframe.py
+++ b/python/sedona/spark/geopandas/geodataframe.py
@@ -1485,9 +1485,103 @@ class GeoDataFrame(GeoFrame, pspd.DataFrame):
@classmethod
def from_features(
- cls, features, crs: Any | None = None, columns: Iterable[str] | None = None
+ cls,
+ features,
+ crs: Any | None = None,
+ columns: typing.Iterable[str] | None = None,
) -> GeoDataFrame:
- raise NotImplementedError("from_features() is not implemented yet.")
+ """Construct a GeoDataFrame from GeoJSON-like features.
+
+ .. versionadded:: 2.0.0
+
+ Parameters
+ ----------
+ features
+ An iterable of feature mappings or objects implementing
+ ``__geo_interface__``; a FeatureCollection dictionary; or an object
+ whose ``__geo_interface__`` is a FeatureCollection.
+ crs : str, dict, pyproj.CRS, optional
+ Coordinate reference system to assign to the resulting frame. The
+ coordinates are not transformed.
+ columns : iterable of str, optional
+ Column names to include and their order in the resulting frame.
+
+ Returns
+ -------
+ GeoDataFrame
+
+ Notes
+ -----
+ This constructor first materializes all features in a local GeoPandas
+ GeoDataFrame on the driver. It is intended for small in-memory feature
+ collections. For large datasets, use Sedona's distributed GeoJSON or
+ GeoParquet Spark data source instead.
+
+ GeoPandas controls feature parsing and column selection; feature ``id``,
+ ``bbox``, and collection metadata are not added as columns. Once the
+ result is distributed, property values follow Spark schema inference:
+ nested mappings become structs, and columns with incompatible mixed
+ types are not supported.
+
+ Examples
+ --------
+ >>> from sedona.spark.geopandas import GeoDataFrame
+ >>> feature_collection = {
+ ... "type": "FeatureCollection",
+ ... "features": [
+ ... {
+ ... "type": "Feature",
+ ... "properties": {"name": "first"},
+ ... "geometry": {
+ ... "type": "Point",
+ ... "coordinates": (1.0, 2.0),
+ ... },
+ ... },
+ ... {
+ ... "type": "Feature",
+ ... "properties": {"name": "second"},
+ ... "geometry": {
+ ... "type": "Point",
+ ... "coordinates": (2.0, 1.0),
+ ... },
+ ... },
+ ... ],
+ ... }
+ >>> GeoDataFrame.from_features(feature_collection)
+ geometry name
+ 0 POINT (1 2) first
+ 1 POINT (2 1) second
+ """
+ local = gpd.GeoDataFrame.from_features(
+ features,
+ crs=crs,
+ columns=columns,
+ )
+
+ # Spark 3 pandas-on-Spark only checks the first object value when
+ # looking for a user-defined type. Move one geometry forward for
+ # schema inference, then restore the GeoPandas RangeIndex order.
+ restore_order = False
+ geometry_name = local._geometry_column_name
+ if geometry_name is not None and len(local) > 0:
+ missing = local[geometry_name].isna().to_numpy()
+ non_missing = np.flatnonzero(~missing)
+ if missing[0] and len(non_missing) > 0:
+ first_geometry = non_missing[0]
+ order = np.concatenate(
+ (
+ [first_geometry],
+ np.arange(first_geometry),
+ np.arange(first_geometry + 1, len(local)),
+ )
+ )
+ local = local.iloc[order]
+ restore_order = True
+
+ result = cls(local)
+ if restore_order:
+ result.sort_index(inplace=True)
+ return result
@classmethod
def from_postgis(
diff --git a/python/tests/geopandas/test_geodataframe.py b/python/tests/geopandas/test_geodataframe.py
index f2cdc07efa4..140b0cc1d25 100644
--- a/python/tests/geopandas/test_geodataframe.py
+++ b/python/tests/geopandas/test_geodataframe.py
@@ -43,6 +43,103 @@ from pandas.testing import assert_frame_equal
from packaging.version import parse as parse_version
+class TestGeoDataFrameFromFeatures(TestGeopandasBase):
+ def test_input_forms(self):
+ feature_collection = {
+ "type": "FeatureCollection",
+ "features": [
+ {
+ "type": "Feature",
+ "properties": {"name": "first"},
+ "geometry": {"type": "Point", "coordinates": (1.0, 2.0)},
+ },
+ {
+ "type": "Feature",
+ "properties": {"name": "second"},
+ "geometry": {"type": "Point", "coordinates": (2.0, 1.0)},
+ },
+ ],
+ }
+
+ class FeatureCollection:
+ __geo_interface__ = feature_collection
+
+ class Feature:
+ def __init__(self, mapping):
+ self.__geo_interface__ = mapping
+
+ expected = gpd.GeoDataFrame.from_features(feature_collection)
+ inputs = [
+ feature_collection,
+ (feature for feature in feature_collection["features"]),
+ FeatureCollection(),
+ [Feature(feature) for feature in feature_collection["features"]],
+ ]
+
+ for features in inputs:
+ result = GeoDataFrame.from_features(features)
+ self.check_sgpd_df_equals_gpd_df(result, expected)
+
+ def test_columns_crs_and_nulls_match_geopandas(self):
+ from geopandas.testing import assert_geodataframe_equal
+
+ features = {
+ "type": "FeatureCollection",
+ "features": [
+ {
+ "id": "first",
+ "type": "Feature",
+ "properties": {"other": 2},
+ "geometry": None,
+ },
+ {
+ "id": "second",
+ "type": "Feature",
+ "properties": {"value": 1},
+ "geometry": {"type": "Point", "coordinates": (1.0, 2.0)},
+ },
+ {
+ "id": "third",
+ "type": "Feature",
+ "properties": None,
+ "geometry": {
+ "type": "LineString",
+ "coordinates": ((0.0, 0.0), (1.0, 1.0)),
+ },
+ },
+ ],
+ }
+ kwargs = {
+ "crs": "EPSG:4326",
+ "columns": ["other", "geometry", "value"],
+ }
+
+ result = GeoDataFrame.from_features(features, **kwargs)
+ expected = gpd.GeoDataFrame.from_features(features, **kwargs)
+ materialized = result.to_geopandas()
+
+ assert list(result.columns) == list(expected.columns)
+ assert result.crs == expected.crs
+ assert materialized.index.tolist() == expected.index.tolist()
+ assert_geodataframe_equal(materialized, expected)
+
+ def test_empty_with_schema(self):
+ expected = gpd.GeoDataFrame.from_features(
+ [],
+ crs="EPSG:4326",
+ columns=["geometry", "name"],
+ )
+ result = GeoDataFrame.from_features(
+ [],
+ crs="EPSG:4326",
+ columns=["geometry", "name"],
+ )
+
+ assert list(result.columns) == list(expected.columns)
+ assert result.crs == expected.crs
+ assert len(result) == 0
+
+
@pytest.mark.skipif(
parse_version(shapely.__version__) < parse_version("2.0.0"),
reason=f"Tests require shapely>=2.0.0, but found v{shapely.__version__}",