Repository: geopandas/geopandas

Issue:
BUG: to_crs() fails if there's None in geometry
With shapely backend, `to_crs()` fails on `None` in geometry column.

```py
import pandas as pd
import geopandas as gpd
from shapely.geometry import Point

gpd.options.use_pygeos = False

df = pd.DataFrame(
    {'City': ['Buenos Aires', 'Brasilia', 'Santiago', 'Bogota', 'Caracas'],
     'Country': ['Argentina', 'Brazil', 'Chile', 'Colombia', 'Venezuela'],
     'Latitude': [-34.58, -15.78, -33.45, 4.60, 10.48],
     'Longitude': [-58.66, -47.91, -70.66, -74.08, -66.86]})

gdf = gpd.GeoDataFrame(
    df, geometry=gpd.points_from_xy(df.Longitude, df.Latitude), crs=4326)

gdf.loc[3,'geometry'] = None
gdf.to_crs(epsg=5347)
```

```
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
 in 
     15 
     16 gdf.loc[3,'geometry'] = None
---> 17 gdf.to_crs(epsg=5347)

/opt/miniconda3/envs/stable/lib/python3.8/site-packages/geopandas/geodataframe.py in to_crs(self, crs, epsg, inplace)
    814         else:
    815             df = self.copy()
--> 816         geom = df.geometry.to_crs(crs=crs, epsg=epsg)
    817         df.geometry = geom
    818         df.crs = geom.crs

/opt/miniconda3/envs/stable/lib/python3.8/site-packages/geopandas/geoseries.py in to_crs(self, crs, epsg)
    541         transformer = Transformer.from_crs(self.crs, crs, always_xy=True)
    542 
--> 543         new_data = vectorized.transform(self.values.data, transformer.transform)
    544         return GeoSeries(
    545             GeometryArray(new_data), crs=crs, index=self.index, name=self.name

/opt/miniconda3/envs/stable/lib/python3.8/site-packages/geopandas/_vectorized.py in transform(data, func)
    887         for i in range(n):
    888             geom = data[i]
--> 889             result[i] = transform(func, geom)
    890 
    891         return result

/opt/miniconda3/envs/stable/lib/python3.8/site-packages/shapely/ops.py in transform(func, geom)
    222     also satisfy the requirements for `func`.
    223     """
--> 224     if geom.is_empty:
    225         return geom
    226     if geom.type in ('Point', 'LineString', 'LinearRing', 'Polygon'):

AttributeError: 'NoneType' object has no attribute 'is_empty'
```

xref #1572 


Discussion:

Resolving patch:
diff --git a/geopandas/_vectorized.py b/geopandas/_vectorized.py
--- a/geopandas/_vectorized.py
+++ b/geopandas/_vectorized.py
@@ -886,6 +886,9 @@ def transform(data, func):
         result = np.empty(n, dtype=object)
         for i in range(n):
             geom = data[i]
-            result[i] = transform(func, geom)
+            if _isna(geom):
+                result[i] = geom
+            else:
+                result[i] = transform(func, geom)
 
         return result


Test patch:
diff --git a/geopandas/tests/test_crs.py b/geopandas/tests/test_crs.py
--- a/geopandas/tests/test_crs.py
+++ b/geopandas/tests/test_crs.py
@@ -49,6 +49,15 @@ def test_to_crs_transform():
     assert_geodataframe_equal(df, utm, check_less_precise=True)
 
 
+def test_to_crs_transform__missing_data():
+    # https://github.com/geopandas/geopandas/issues/1573
+    df = df_epsg26918()
+    df.loc[3, "geometry"] = None
+    lonlat = df.to_crs(epsg=4326)
+    utm = lonlat.to_crs(epsg=26918)
+    assert_geodataframe_equal(df, utm, check_less_precise=True)
+
+
 def test_to_crs_inplace():
     df = df_epsg26918()
     lonlat = df.to_crs(epsg=4326)