From 56fd62f010da0efdac44b0186e27cb9893ae2fb8 Mon Sep 17 00:00:00 2001 From: James McBride Date: Fri, 19 Feb 2021 01:13:33 -0800 Subject: [PATCH] ENH: Make the id property in to_json optional (#1637) * Make the id property in to_json optional As discussed in #390, the id property in geojson generated with to_json may not be all that meaningful (frequently just an arbitrary row number in a dataframe). The option to disable writing ids without meaning is given here with a new argument `drop_id`, set to `False` by default. * Add to_json tests when only geometry column * Fix linting error * Improved docstring on drop_id option * Fix merge conflict leftover * Set id key first in iterfeatures * Update geopandas/geodataframe.py * Update geopandas/geodataframe.py Co-authored-by: Martin Fleischmann Co-authored-by: Joris Van den Bossche --- geopandas/geodataframe.py | 57 ++++++++++++++++++---------- geopandas/tests/test_geodataframe.py | 21 ++++++++++ 2 files changed, 59 insertions(+), 19 deletions(-) diff --git a/geopandas/geodataframe.py b/geopandas/geodataframe.py index 09188ef..78fc3a3 100644 --- a/geopandas/geodataframe.py +++ b/geopandas/geodataframe.py @@ -658,7 +658,7 @@ class GeoDataFrame(GeoPandasBase, DataFrame): return df - def to_json(self, na="null", show_bbox=False, **kwargs): + def to_json(self, na="null", show_bbox=False, drop_id=False, **kwargs): """ Returns a GeoJSON representation of the ``GeoDataFrame`` as a string. @@ -669,6 +669,10 @@ class GeoDataFrame(GeoPandasBase, DataFrame): See below. show_bbox : bool, optional, default: False Include bbox (bounds) in the geojson + drop_id : bool, default: False + Whether to retain the index of the GeoDataFrame as the id property + in the generated GeoJSON. Default is False, but may want True + if the index is just arbitrary row numbers. Notes ----- @@ -707,7 +711,9 @@ class GeoDataFrame(GeoPandasBase, DataFrame): GeoDataFrame.to_file : write GeoDataFrame to file """ - return json.dumps(self._to_geo(na=na, show_bbox=show_bbox), **kwargs) + return json.dumps( + self._to_geo(na=na, show_bbox=show_bbox, drop_id=drop_id), **kwargs + ) @property def __geo_interface__(self): @@ -740,24 +746,29 @@ box': (2.0, 1.0, 2.0, 1.0)}], 'bbox': (1.0, 1.0, 2.0, 2.0)} """ - return self._to_geo(na="null", show_bbox=True) + return self._to_geo(na="null", show_bbox=True, drop_id=False) - def iterfeatures(self, na="null", show_bbox=False): + def iterfeatures(self, na="null", show_bbox=False, drop_id=False): """ Returns an iterator that yields feature dictionaries that comply with __geo_interface__ Parameters ---------- - na : {'null', 'drop', 'keep'}, default 'null' + na : str, optional + Options are {'null', 'drop', 'keep'}, default 'null'. Indicates how to output missing (NaN) values in the GeoDataFrame * null: ouput the missing entries as JSON null * drop: remove the property from the feature. This applies to each feature individually so that features may have different properties * keep: output the missing entries as NaN - - show_bbox : include bbox (bounds) in the geojson. default False + show_bbox : bool, optional + Include bbox (bounds) in the geojson. Default False. + drop_id : bool, default: False + Whether to retain the index of the GeoDataFrame as the id property + in the generated GeoJSON. Default is False, but may want True + if the index is just arbitrary row numbers. Examples -------- @@ -805,27 +816,35 @@ box': (2.0, 1.0, 2.0, 1.0)}], 'bbox': (1.0, 1.0, 2.0, 2.0)} else: properties_items = {k: v for k, v in zip(properties_cols, row)} - feature = { - "id": str(ids[i]), - "type": "Feature", - "properties": properties_items, - "geometry": mapping(geom) if geom else None, - } + if drop_id: + feature = {} + else: + feature = {"id": str(ids[i])} + + feature["type"] = "Feature" + feature["properties"] = properties_items + feature["geometry"] = mapping(geom) if geom else None if show_bbox: feature["bbox"] = geom.bounds if geom else None + yield feature else: for fid, geom in zip(ids, geometries): - feature = { - "id": str(fid), - "type": "Feature", - "properties": {}, - "geometry": mapping(geom) if geom else None, - } + + if drop_id: + feature = {} + else: + feature = {"id": str(fid)} + + feature["type"] = "Feature" + feature["properties"] = {} + feature["geometry"] = mapping(geom) if geom else None + if show_bbox: feature["bbox"] = geom.bounds if geom else None + yield feature def _to_geo(self, **kwargs): diff --git a/geopandas/tests/test_geodataframe.py b/geopandas/tests/test_geodataframe.py index 28b1e84..47c38aa 100644 --- a/geopandas/tests/test_geodataframe.py +++ b/geopandas/tests/test_geodataframe.py @@ -365,6 +365,7 @@ class TestDataFrame: data = json.loads(text) assert data["type"] == "FeatureCollection" assert len(data["features"]) == 5 + assert "id" in data["features"][0].keys() def test_to_json_geom_col(self): df = self.df.copy() @@ -377,6 +378,12 @@ class TestDataFrame: assert data["type"] == "FeatureCollection" assert len(data["features"]) == 5 + def test_to_json_only_geom_column(self): + text = self.df[["geometry"]].to_json() + data = json.loads(text) + assert len(data["features"]) == 5 + assert "id" in data["features"][0].keys() + def test_to_json_na(self): # Set a value as nan and make sure it's written self.df.loc[self.df["BoroName"] == "Queens", "Shape_Area"] = np.nan @@ -436,6 +443,20 @@ class TestDataFrame: assert np.isnan(props["Shape_Leng"]) assert "Shape_Area" in props + def test_to_json_drop_id(self): + text = self.df.to_json(drop_id=True) + data = json.loads(text) + assert len(data["features"]) == 5 + for f in data["features"]: + assert "id" not in f.keys() + + def test_to_json_drop_id_only_geom_column(self): + text = self.df[["geometry"]].to_json(drop_id=True) + data = json.loads(text) + assert len(data["features"]) == 5 + for f in data["features"]: + assert "id" not in f.keys() + def test_copy(self): df2 = self.df.copy() assert type(df2) is GeoDataFrame