Repository: geopandas/geopandas

Issue:
The x, y, z coordinate attributes error with empty points
Hi! Working with GeoPandas `0.10.2` and the method [here ](https://geopandas.readthedocs.io/en/stable/docs/reference/api/geopandas.GeoSeries.z.html#geopandas.GeoSeries.z) fails:

```python
>>> my_geodataframe.geometry.x
...
IndexError: index out of range
```
 
See the full traceback attached: [traceback.log](https://github.com/geopandas/geopandas/files/7503163/traceback.log).

Ideally, however, this should work: with `my_geodataframe.geometry` I get a `GeoSeries`, so the `.[xyx]` attribute should return the longitude/latitude/elevation.

_Originally posted by @baggiponte in https://github.com/geopandas/geopandas/issues/246#issuecomment-963926763_


Discussion:
@baggiponte that's because of the presence of empty points, it seems.

Small reproducer:

```python
import geopandas
from shapely import wkt

gs = geopandas.GeoSeries([wkt.loads("POINT (1 1)"), wkt.loads("POINT EMPTY")])
gs.x
```

Both using pygeos or shapely under the hood errors in the case above, and that simply bubbles up the error from shapely/pygeos itself:

```python
# Shapely
>>> from shapely import wkt
>>> empty = wkt.loads("POINT EMPTY")
>>> empty.x
Traceback (most recent call last)
~/miniconda3/envs/geo-dev/lib/python3.9/site-packages/shapely/geometry/point.py in x(self)
~/miniconda3/envs/geo-dev/lib/python3.9/site-packages/shapely/coords.py in __getitem__(self, key)
IndexError: index out of range

# pygeos
>>> import pygeos
>>> empty = pygeos.Geometry("POINT EMPTY")
>>> pygeos.get_x(empty)
Traceback (most recent call last)
...
GEOSException: UnsupportedOperationException: getX called on empty Point
```


Oh I completely ignored this! Thank you very much for the help. I also tried this, which confirms your suspicions:

```python
>>> my_data[my_data.geometry.isna()]
UserWarning: GeoSeries.isna() previously returned True for both missing (None) and empty geometries. Now, it only returns True for missing values. Since the calling GeoSeries contains empty geometries, the result has changed compared to previous versions of GeoPandas.
Given a GeoSeries 's', you can use 's.is_empty | s.isna()' to get back the old behaviour.
```

Can I perhaps do something, like help with drafting a more informative error message for the traceback or on the official documentation?
Would returning `nan` for empty geometry coordinates be more appropriate?
> Would returning `nan` for empty geometry coordinates be more appropriate?

Yeah, that is probably the only solution if we don't want to raise an error.

Resolving patch:
diff --git a/geopandas/array.py b/geopandas/array.py
--- a/geopandas/array.py
+++ b/geopandas/array.py
@@ -879,7 +879,14 @@ def estimate_utm_crs(self, datum_name="WGS 84"):
     def x(self):
         """Return the x location of point geometries in a GeoSeries"""
         if (self.geom_type[~self.isna()] == "Point").all():
-            return vectorized.get_x(self.data)
+            empty = self.is_empty
+            if empty.any():
+                nonempty = ~empty
+                coords = np.full_like(nonempty, dtype=float, fill_value=np.nan)
+                coords[nonempty] = vectorized.get_x(self.data[nonempty])
+                return coords
+            else:
+                return vectorized.get_x(self.data)
         else:
             message = "x attribute access only provided for Point geometries"
             raise ValueError(message)
@@ -888,7 +895,14 @@ def x(self):
     def y(self):
         """Return the y location of point geometries in a GeoSeries"""
         if (self.geom_type[~self.isna()] == "Point").all():
-            return vectorized.get_y(self.data)
+            empty = self.is_empty
+            if empty.any():
+                nonempty = ~empty
+                coords = np.full_like(nonempty, dtype=float, fill_value=np.nan)
+                coords[nonempty] = vectorized.get_y(self.data[nonempty])
+                return coords
+            else:
+                return vectorized.get_y(self.data)
         else:
             message = "y attribute access only provided for Point geometries"
             raise ValueError(message)
@@ -897,7 +911,14 @@ def y(self):
     def z(self):
         """Return the z location of point geometries in a GeoSeries"""
         if (self.geom_type[~self.isna()] == "Point").all():
-            return vectorized.get_z(self.data)
+            empty = self.is_empty
+            if empty.any():
+                nonempty = ~empty
+                coords = np.full_like(nonempty, dtype=float, fill_value=np.nan)
+                coords[nonempty] = vectorized.get_z(self.data[nonempty])
+                return coords
+            else:
+                return vectorized.get_z(self.data)
         else:
             message = "z attribute access only provided for Point geometries"
             raise ValueError(message)


Test patch:
diff --git a/geopandas/tests/test_geom_methods.py b/geopandas/tests/test_geom_methods.py
--- a/geopandas/tests/test_geom_methods.py
+++ b/geopandas/tests/test_geom_methods.py
@@ -7,6 +7,7 @@
 from shapely.geometry import LinearRing, LineString, MultiPoint, Point, Polygon
 from shapely.geometry.collection import GeometryCollection
 from shapely.ops import unary_union
+from shapely import wkt
 
 from geopandas import GeoDataFrame, GeoSeries
 from geopandas.base import GeoPandasBase
@@ -72,6 +73,10 @@ def setup_method(self):
         self.landmarks = GeoSeries([self.esb, self.sol], crs="epsg:4326")
         self.pt2d = Point(-73.9847, 40.7484)
         self.landmarks_mixed = GeoSeries([self.esb, self.sol, self.pt2d], crs=4326)
+        self.pt_empty = wkt.loads("POINT EMPTY")
+        self.landmarks_mixed_empty = GeoSeries(
+            [self.esb, self.sol, self.pt2d, self.pt_empty], crs=4326
+        )
         self.l1 = LineString([(0, 0), (0, 1), (1, 1)])
         self.l2 = LineString([(0, 0), (1, 0), (1, 1), (0, 1)])
         self.g5 = GeoSeries([self.l1, self.l2])
@@ -617,6 +622,15 @@ def test_xyz_points(self):
         expected_z = [30.3244, 31.2344, np.nan]
         assert_array_dtype_equal(expected_z, self.landmarks_mixed.geometry.z)
 
+    def test_xyz_points_empty(self):
+        expected_x = [-73.9847, -74.0446, -73.9847, np.nan]
+        expected_y = [40.7484, 40.6893, 40.7484, np.nan]
+        expected_z = [30.3244, 31.2344, np.nan, np.nan]
+
+        assert_array_dtype_equal(expected_x, self.landmarks_mixed_empty.geometry.x)
+        assert_array_dtype_equal(expected_y, self.landmarks_mixed_empty.geometry.y)
+        assert_array_dtype_equal(expected_z, self.landmarks_mixed_empty.geometry.z)
+
     def test_xyz_polygons(self):
         # accessing x attribute in polygon geoseries should raise an error
         with pytest.raises(ValueError):