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 <martin@martinfleischmann.net>
Co-authored-by: Joris Van den Bossche <jorisvandenbossche@gmail.com>
This commit is contained in:
James McBride
2021-02-19 09:13:33 +00:00
committed by GitHub
co-authored by Martin Fleischmann Joris Van den Bossche
parent 8bc5c30afd
commit 56fd62f010
2 changed files with 59 additions and 19 deletions
+38 -19
View File
@@ -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):
+21
View File
@@ -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