Repository: geopandas/geopandas

Issue:
PERF: improve overlay performance
This improves the performance of the overlay on the countries/capitals dataset from 25.9 s to 12.5 s on my computer.

I did not change the logic of the implementation (looking at that could maybe also give some additional performance reduction opportunities), but just optimized the current logic, including:
- not concatting inside the for loop, but collecting the row labels and concatting once after the for loop
- using `iat` instead of `ix`

Tested with (example from the docs):

```
import geopandas as gpd
world = gpd.read_file(gpd.datasets.get_path('naturalearth_lowres'))
capitals = gpd.read_file(gpd.datasets.get_path('naturalearth_cities'))
countries = world[['geometry', 'name']]
countries = countries.to_crs('+init=epsg:3395')[countries.name!="Antarctica"]
capitals = capitals.to_crs('+init=epsg:3395')
capitals['geometry']= capitals.buffer(500000)

gpd.overlay(countries, capitals, how='intersection')
```

TODO:
- [ ] add some more tests (including tests for dataframes with non default (0,1,2, ...) row indices)

BUG? overlay with overlapping geometries in input layer
From https://stackoverflow.com/questions/48655331/geopandas-overlay-intersection-doesnt-work-as-expected

I made a small reproducible and illustrating script:

```
store = geopandas.GeoDataFrame({'store_area': [1, 2],
                                'geometry': [Polygon([(0, 0), (3, 0), (3, 3), (0, 3)]),
                                             Polygon([(2, 2), (5, 2), (5, 5), (2, 5)])]})
area = geopandas.GeoDataFrame({'geometry': [Polygon([(1, 1), (1, 4), (4, 4), (4, 1)])]})

ax = store.plot(alpha=0.5)
area.plot(ax=ax, facecolor='none', edgecolor='red', linewidth=2)
```
![image](https://user-images.githubusercontent.com/1020496/35910674-a50ab12a-0bf7-11e8-8f66-873c44b979f3.png)


This produces:

```
>>> geopandas.overlay(store, area, how='intersection')

   store_area                                       geometry
0           1  POLYGON ((3 2, 3 1, 1 1, 1 3, 2 3, 2 2, 3 2))
1           2  POLYGON ((3 2, 3 3, 2 3, 2 4, 4 4, 4 2, 3 2))
2           1            POLYGON ((2 3, 3 3, 3 2, 2 2, 2 3))
```

So the small overlapping square in the middle is a separate feature in the result (it also got the 'store_area' of 1, while it is actually contained in both 1 and 2). 

I am not fully sure what *should* be the result, but it looks like this should be 2 features (that still overlap). That is also what QGIS 'intersection' overlay operation does.




Discussion:
[![Coverage Status](https://coveralls.io/builds/6486303/badge)](https://coveralls.io/builds/6486303)

Coverage decreased (-0.01%) to 91.296% when pulling **ab0a8064c2792934a862e87fc7a7bc18063aff02 on jorisvandenbossche:perf-overlay** into **752ab5ddb703eaf6cc8148ea7d588f1ee4499325 on geopandas:master**.

Have you profiled this? I had assumed issues were coming from the use of `iterrows` at line 35? (thus #330)

Yes, I line profiled it, and the two calls to the `_extract_rings()` function only takes 0.6% of the time (of course, for this example).

@jorisvandenbossche oh, great! Shows the illustration of profiling rather than guessing. :)



Resolving patch:
diff --git a/geopandas/tools/overlay.py b/geopandas/tools/overlay.py
--- a/geopandas/tools/overlay.py
+++ b/geopandas/tools/overlay.py
@@ -1,3 +1,7 @@
+import warnings
+from functools import reduce
+
+import numpy as np
 import pandas as pd
 from shapely.ops import unary_union, polygonize
 from shapely.geometry import MultiLineString
@@ -56,7 +60,7 @@ def _extract_rings(df):
     return rings
 
 
-def overlay(df1, df2, how, use_sindex=True):
+def _overlay_old(df1, df2, how, use_sindex=True, **kwargs):
     """Perform spatial overlay between two polygons.
 
     Currently only supports data GeoDataFrames with polygons.
@@ -176,3 +180,191 @@ def overlay(df1, df2, how, use_sindex=True):
 
     # Return geodataframe with new indices
     return GeoDataFrame(collection, index=range(len(collection)))
+
+
+def _ensure_geometry_column(df):
+    """
+    Helper function to ensure the geometry column is called 'geometry'.
+    If another column with that name exists, it will be dropped.
+    """
+    if not df._geometry_column_name == 'geometry':
+        if 'geometry' in df.columns:
+            df.drop('geometry', axis=1, inplace=True)
+        df.rename(columns={df._geometry_column_name: 'geometry'},
+                  copy=False, inplace=True)
+        df.set_geometry('geometry', inplace=True)
+
+
+def _overlay_intersection(df1, df2):
+    """
+    Overlay Intersection operation used in overlay function
+    """
+    # Spatial Index to create intersections
+    spatial_index = df2.sindex
+    bbox = df1.geometry.apply(lambda x: x.bounds)
+    sidx = bbox.apply(lambda x: list(spatial_index.intersection(x)))
+    # Create pairs of geometries in both dataframes to be intersected
+    nei = []
+    for i, j in enumerate(sidx):
+        for k in j:
+            nei.append([i, k])
+    if nei != []:
+        pairs = pd.DataFrame(nei, columns=['__idx1', '__idx2'])
+        left = df1.geometry.take(pairs['__idx1'].values)
+        left.reset_index(drop=True, inplace=True)
+        right = df2.geometry.take(pairs['__idx2'].values)
+        right.reset_index(drop=True, inplace=True)
+        intersections = left.intersection(right).buffer(0)
+
+        # only keep actual intersecting geometries
+        pairs_intersect = pairs[~intersections.is_empty]
+        geom_intersect = intersections[~intersections.is_empty]
+
+        # merge data for intersecting geometries
+        df1 = df1.reset_index(drop=True)
+        df2 = df2.reset_index(drop=True)
+        dfinter = pairs_intersect.merge(
+            df1.drop(df1._geometry_column_name, axis=1),
+            left_on='__idx1', right_index=True)
+        dfinter = dfinter.merge(
+            df2.drop(df2._geometry_column_name, axis=1),
+            left_on='__idx2', right_index=True, suffixes=['_1', '_2'])
+
+        return GeoDataFrame(dfinter, geometry=geom_intersect, crs=df1.crs)
+    else:
+        return GeoDataFrame(
+            [],
+            columns=list(set(df1.columns).union(df2.columns)),
+            crs=df1.crs)
+
+
+def _overlay_difference(df1, df2):
+    """
+    Overlay Difference operation used in overlay function
+    """
+    # Spatial Index to create intersections
+    spatial_index = df2.sindex
+    bbox = df1.geometry.apply(lambda x: x.bounds)
+    sidx = bbox.apply(lambda x: list(spatial_index.intersection(x)))
+    # Create differences
+    new_g = []
+    for geom, neighbours in zip(df1.geometry, sidx):
+        new = reduce(lambda x, y: x.difference(y).buffer(0),
+                     [geom] + list(df2.geometry.iloc[neighbours]))
+        new_g.append(new)
+    differences = GeoSeries(new_g, index=df1.index)
+    geom_diff = differences[~differences.is_empty].copy()
+    dfdiff = df1[~differences.is_empty].copy()
+    dfdiff[dfdiff._geometry_column_name] = geom_diff
+    return dfdiff
+
+
+def _overlay_symmetric_diff(df1, df2):
+    """
+    Overlay Symmetric Difference operation used in overlay function
+    """
+    dfdiff1 = _overlay_difference(df1, df2)
+    dfdiff2 = _overlay_difference(df2, df1)
+    dfdiff1['__idx1'] = range(len(dfdiff1))
+    dfdiff2['__idx2'] = range(len(dfdiff2))
+    dfdiff1['__idx2'] = np.nan
+    dfdiff2['__idx1'] = np.nan
+    # ensure geometry name (otherwise merge goes wrong)
+    _ensure_geometry_column(dfdiff1)
+    _ensure_geometry_column(dfdiff2)
+    # combine both 'difference' dataframes
+    dfsym = dfdiff1.merge(dfdiff2, on=['__idx1', '__idx2'], how='outer',
+                          suffixes=['_1', '_2'])
+    geometry = dfsym.geometry_1.copy()
+    geometry[dfsym.geometry_1.isnull()] = \
+        dfsym.loc[dfsym.geometry_1.isnull(), 'geometry_2']
+    dfsym.drop(['geometry_1', 'geometry_2'], axis=1, inplace=True)
+    dfsym.reset_index(drop=True, inplace=True)
+    dfsym = GeoDataFrame(dfsym, geometry=geometry, crs=df1.crs)
+    return dfsym
+
+
+def _overlay_union(df1, df2):
+    """
+    Overlay Union operation used in overlay function
+    """
+    dfinter = _overlay_intersection(df1, df2)
+    dfsym = _overlay_symmetric_diff(df1, df2)
+    dfunion = pd.concat([dfinter, dfsym], ignore_index=True)
+    # keep geometry column last
+    columns = list(dfunion.columns)
+    columns.remove('geometry')
+    columns = columns + ['geometry']
+    return dfunion.reindex(columns=columns)
+
+
+def overlay(df1, df2, how='intersection', make_valid=True, use_sindex=None):
+    """Perform spatial overlay between two polygons.
+
+    Currently only supports data GeoDataFrames with polygons.
+    Implements several methods that are all effectively subsets of
+    the union.
+
+    Parameters
+    ----------
+    df1 : GeoDataFrame with MultiPolygon or Polygon geometry column
+    df2 : GeoDataFrame with MultiPolygon or Polygon geometry column
+    how : string
+        Method of spatial overlay: 'intersection', 'union',
+        'identity', 'symmetric_difference' or 'difference'.
+
+    Returns
+    -------
+    df : GeoDataFrame
+        GeoDataFrame with new set of polygons and attributes
+        resulting from the overlay
+
+    """
+    if use_sindex is not None:
+        warnings.warn("'use_sindex' is deprecated. The overlay operation "
+                      "always requires a spatial index (rtree).",
+                      DeprecationWarning, stacklevel=2)
+
+    # Allowed operations
+    allowed_hows = [
+        'intersection',
+        'union',
+        'identity',
+        'symmetric_difference',
+        'difference',  # aka erase
+    ]
+    # Error Messages
+    if how not in allowed_hows:
+        raise ValueError("`how` was '{0}' but is expected to be "
+                         "in %s".format(how, allowed_hows))
+
+    if isinstance(df1, GeoSeries) or isinstance(df2, GeoSeries):
+        raise NotImplementedError("overlay currently only implemented for "
+                                  "GeoDataFrames")
+
+    accepted_types = ['Polygon', 'MultiPolygon']
+    if (not df1.geom_type.isin(accepted_types).all()
+            or not df2.geom_type.isin(accepted_types).all()):
+        raise TypeError("overlay only takes GeoDataFrames with (multi)polygon "
+                        " geometries.")
+
+    # Computations
+    df1 = df1.copy()
+    df2 = df2.copy()
+    df1[df1._geometry_column_name] = df1.geometry.buffer(0)
+    df2[df2._geometry_column_name] = df2.geometry.buffer(0)
+
+    if how == 'difference':
+        return _overlay_difference(df1, df2)
+    elif how == 'intersection':
+        result = _overlay_intersection(df1, df2)
+    elif how == 'symmetric_difference':
+        result = _overlay_symmetric_diff(df1, df2)
+    elif how == 'union':
+        result = _overlay_union(df1, df2)
+    elif how == 'identity':
+        dfunion = _overlay_union(df1, df2)
+        result = dfunion[dfunion['__idx1'].notnull()].copy()
+        result.reset_index(drop=True, inplace=True)
+    result.drop(['__idx1', '__idx2'], axis=1, inplace=True)
+    return result
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -41,6 +41,8 @@
         elif item.endswith('.zip'):
             data_files.append(os.path.join("datasets", item))
 
+data_files.append('tests/data/*')
+
 
 setup(name='geopandas',
       version=versioneer.get_version(),


Test patch:
diff --git a/geopandas/tests/data/overlay/nybb_qgis/polydf.cpg b/geopandas/tests/data/overlay/nybb_qgis/polydf.cpg
new file mode 100644
--- /dev/null
+++ b/geopandas/tests/data/overlay/nybb_qgis/polydf.cpg
@@ -0,0 +1 @@
+ISO-8859-1
\ No newline at end of file
diff --git a/geopandas/tests/data/overlay/nybb_qgis/polydf.dbf b/geopandas/tests/data/overlay/nybb_qgis/polydf.dbf
new file mode 100644
Binary files /dev/null and b/geopandas/tests/data/overlay/nybb_qgis/polydf.dbf differ
diff --git a/geopandas/tests/data/overlay/nybb_qgis/polydf.prj b/geopandas/tests/data/overlay/nybb_qgis/polydf.prj
new file mode 100644
--- /dev/null
+++ b/geopandas/tests/data/overlay/nybb_qgis/polydf.prj
@@ -0,0 +1 @@
+PROJCS["Lambert_Conformal_Conic",GEOGCS["GCS_North_American_1983",DATUM["D_North_American_1983",SPHEROID["GRS_1980",6378137,298.257222101]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]],PROJECTION["Lambert_Conformal_Conic"],PARAMETER["standard_parallel_1",40.66666666666666],PARAMETER["standard_parallel_2",41.03333333333333],PARAMETER["latitude_of_origin",40.16666666666666],PARAMETER["central_meridian",-74],PARAMETER["false_easting",984250],PARAMETER["false_northing",0],UNIT["Foot_US",0.30480060960121924]]
\ No newline at end of file
diff --git a/geopandas/tests/data/overlay/nybb_qgis/polydf.shp b/geopandas/tests/data/overlay/nybb_qgis/polydf.shp
new file mode 100644
Binary files /dev/null and b/geopandas/tests/data/overlay/nybb_qgis/polydf.shp differ
diff --git a/geopandas/tests/data/overlay/nybb_qgis/polydf.shx b/geopandas/tests/data/overlay/nybb_qgis/polydf.shx
new file mode 100644
Binary files /dev/null and b/geopandas/tests/data/overlay/nybb_qgis/polydf.shx differ
diff --git a/geopandas/tests/data/overlay/nybb_qgis/polydf2.cpg b/geopandas/tests/data/overlay/nybb_qgis/polydf2.cpg
new file mode 100644
--- /dev/null
+++ b/geopandas/tests/data/overlay/nybb_qgis/polydf2.cpg
@@ -0,0 +1 @@
+ISO-8859-1
\ No newline at end of file
diff --git a/geopandas/tests/data/overlay/nybb_qgis/polydf2.dbf b/geopandas/tests/data/overlay/nybb_qgis/polydf2.dbf
new file mode 100644
Binary files /dev/null and b/geopandas/tests/data/overlay/nybb_qgis/polydf2.dbf differ
diff --git a/geopandas/tests/data/overlay/nybb_qgis/polydf2.prj b/geopandas/tests/data/overlay/nybb_qgis/polydf2.prj
new file mode 100644
--- /dev/null
+++ b/geopandas/tests/data/overlay/nybb_qgis/polydf2.prj
@@ -0,0 +1 @@
+PROJCS["Lambert_Conformal_Conic",GEOGCS["GCS_North_American_1983",DATUM["D_North_American_1983",SPHEROID["GRS_1980",6378137,298.257222101]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]],PROJECTION["Lambert_Conformal_Conic"],PARAMETER["standard_parallel_1",40.66666666666666],PARAMETER["standard_parallel_2",41.03333333333333],PARAMETER["latitude_of_origin",40.16666666666666],PARAMETER["central_meridian",-74],PARAMETER["false_easting",984250],PARAMETER["false_northing",0],UNIT["Foot_US",0.30480060960121924]]
\ No newline at end of file
diff --git a/geopandas/tests/data/overlay/nybb_qgis/polydf2.shp b/geopandas/tests/data/overlay/nybb_qgis/polydf2.shp
new file mode 100644
Binary files /dev/null and b/geopandas/tests/data/overlay/nybb_qgis/polydf2.shp differ
diff --git a/geopandas/tests/data/overlay/nybb_qgis/polydf2.shx b/geopandas/tests/data/overlay/nybb_qgis/polydf2.shx
new file mode 100644
Binary files /dev/null and b/geopandas/tests/data/overlay/nybb_qgis/polydf2.shx differ
diff --git a/geopandas/tests/data/overlay/nybb_qgis/qgis-difference.dbf b/geopandas/tests/data/overlay/nybb_qgis/qgis-difference.dbf
new file mode 100644
Binary files /dev/null and b/geopandas/tests/data/overlay/nybb_qgis/qgis-difference.dbf differ
diff --git a/geopandas/tests/data/overlay/nybb_qgis/qgis-difference.prj b/geopandas/tests/data/overlay/nybb_qgis/qgis-difference.prj
new file mode 100644
--- /dev/null
+++ b/geopandas/tests/data/overlay/nybb_qgis/qgis-difference.prj
@@ -0,0 +1 @@
+PROJCS["Lambert_Conformal_Conic",GEOGCS["GCS_GRS 1980(IUGG, 1980)",DATUM["D_unknown",SPHEROID["GRS80",6378137,298.257222101]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]],PROJECTION["Lambert_Conformal_Conic"],PARAMETER["standard_parallel_1",40.66666666666666],PARAMETER["standard_parallel_2",41.03333333333333],PARAMETER["latitude_of_origin",40.16666666666666],PARAMETER["central_meridian",-74],PARAMETER["false_easting",984250],PARAMETER["false_northing",0],UNIT["Foot_US",0.30480060960121924]]
\ No newline at end of file
diff --git a/geopandas/tests/data/overlay/nybb_qgis/qgis-difference.qpj b/geopandas/tests/data/overlay/nybb_qgis/qgis-difference.qpj
new file mode 100644
--- /dev/null
+++ b/geopandas/tests/data/overlay/nybb_qgis/qgis-difference.qpj
@@ -0,0 +1 @@
+PROJCS["unnamed",GEOGCS["GRS 1980(IUGG, 1980)",DATUM["unknown",SPHEROID["GRS80",6378137,298.257222101],TOWGS84[0,0,0,0,0,0,0]],PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433]],PROJECTION["Lambert_Conformal_Conic_2SP"],PARAMETER["standard_parallel_1",40.66666666666666],PARAMETER["standard_parallel_2",41.03333333333333],PARAMETER["latitude_of_origin",40.16666666666666],PARAMETER["central_meridian",-74],PARAMETER["false_easting",984250],PARAMETER["false_northing",0],UNIT["Foot_US",0.3048006096012192]]
diff --git a/geopandas/tests/data/overlay/nybb_qgis/qgis-difference.shp b/geopandas/tests/data/overlay/nybb_qgis/qgis-difference.shp
new file mode 100644
Binary files /dev/null and b/geopandas/tests/data/overlay/nybb_qgis/qgis-difference.shp differ
diff --git a/geopandas/tests/data/overlay/nybb_qgis/qgis-difference.shx b/geopandas/tests/data/overlay/nybb_qgis/qgis-difference.shx
new file mode 100644
Binary files /dev/null and b/geopandas/tests/data/overlay/nybb_qgis/qgis-difference.shx differ
diff --git a/geopandas/tests/data/overlay/nybb_qgis/qgis-intersection.dbf b/geopandas/tests/data/overlay/nybb_qgis/qgis-intersection.dbf
new file mode 100644
Binary files /dev/null and b/geopandas/tests/data/overlay/nybb_qgis/qgis-intersection.dbf differ
diff --git a/geopandas/tests/data/overlay/nybb_qgis/qgis-intersection.prj b/geopandas/tests/data/overlay/nybb_qgis/qgis-intersection.prj
new file mode 100644
--- /dev/null
+++ b/geopandas/tests/data/overlay/nybb_qgis/qgis-intersection.prj
@@ -0,0 +1 @@
+PROJCS["Lambert_Conformal_Conic",GEOGCS["GCS_GRS 1980(IUGG, 1980)",DATUM["D_unknown",SPHEROID["GRS80",6378137,298.257222101]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]],PROJECTION["Lambert_Conformal_Conic"],PARAMETER["standard_parallel_1",40.66666666666666],PARAMETER["standard_parallel_2",41.03333333333333],PARAMETER["latitude_of_origin",40.16666666666666],PARAMETER["central_meridian",-74],PARAMETER["false_easting",984250],PARAMETER["false_northing",0],UNIT["Foot_US",0.30480060960121924]]
\ No newline at end of file
diff --git a/geopandas/tests/data/overlay/nybb_qgis/qgis-intersection.qpj b/geopandas/tests/data/overlay/nybb_qgis/qgis-intersection.qpj
new file mode 100644
--- /dev/null
+++ b/geopandas/tests/data/overlay/nybb_qgis/qgis-intersection.qpj
@@ -0,0 +1 @@
+PROJCS["unnamed",GEOGCS["GRS 1980(IUGG, 1980)",DATUM["unknown",SPHEROID["GRS80",6378137,298.257222101],TOWGS84[0,0,0,0,0,0,0]],PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433]],PROJECTION["Lambert_Conformal_Conic_2SP"],PARAMETER["standard_parallel_1",40.66666666666666],PARAMETER["standard_parallel_2",41.03333333333333],PARAMETER["latitude_of_origin",40.16666666666666],PARAMETER["central_meridian",-74],PARAMETER["false_easting",984250],PARAMETER["false_northing",0],UNIT["Foot_US",0.3048006096012192]]
diff --git a/geopandas/tests/data/overlay/nybb_qgis/qgis-intersection.shp b/geopandas/tests/data/overlay/nybb_qgis/qgis-intersection.shp
new file mode 100644
Binary files /dev/null and b/geopandas/tests/data/overlay/nybb_qgis/qgis-intersection.shp differ
diff --git a/geopandas/tests/data/overlay/nybb_qgis/qgis-intersection.shx b/geopandas/tests/data/overlay/nybb_qgis/qgis-intersection.shx
new file mode 100644
Binary files /dev/null and b/geopandas/tests/data/overlay/nybb_qgis/qgis-intersection.shx differ
diff --git a/geopandas/tests/data/overlay/nybb_qgis/qgis-symmetric_difference.dbf b/geopandas/tests/data/overlay/nybb_qgis/qgis-symmetric_difference.dbf
new file mode 100644
Binary files /dev/null and b/geopandas/tests/data/overlay/nybb_qgis/qgis-symmetric_difference.dbf differ
diff --git a/geopandas/tests/data/overlay/nybb_qgis/qgis-symmetric_difference.prj b/geopandas/tests/data/overlay/nybb_qgis/qgis-symmetric_difference.prj
new file mode 100644
--- /dev/null
+++ b/geopandas/tests/data/overlay/nybb_qgis/qgis-symmetric_difference.prj
@@ -0,0 +1 @@
+PROJCS["Lambert_Conformal_Conic",GEOGCS["GCS_GRS 1980(IUGG, 1980)",DATUM["D_unknown",SPHEROID["GRS80",6378137,298.257222101]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]],PROJECTION["Lambert_Conformal_Conic"],PARAMETER["standard_parallel_1",40.66666666666666],PARAMETER["standard_parallel_2",41.03333333333333],PARAMETER["latitude_of_origin",40.16666666666666],PARAMETER["central_meridian",-74],PARAMETER["false_easting",984250],PARAMETER["false_northing",0],UNIT["Foot_US",0.30480060960121924]]
\ No newline at end of file
diff --git a/geopandas/tests/data/overlay/nybb_qgis/qgis-symmetric_difference.qpj b/geopandas/tests/data/overlay/nybb_qgis/qgis-symmetric_difference.qpj
new file mode 100644
--- /dev/null
+++ b/geopandas/tests/data/overlay/nybb_qgis/qgis-symmetric_difference.qpj
@@ -0,0 +1 @@
+PROJCS["unnamed",GEOGCS["GRS 1980(IUGG, 1980)",DATUM["unknown",SPHEROID["GRS80",6378137,298.257222101],TOWGS84[0,0,0,0,0,0,0]],PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433]],PROJECTION["Lambert_Conformal_Conic_2SP"],PARAMETER["standard_parallel_1",40.66666666666666],PARAMETER["standard_parallel_2",41.03333333333333],PARAMETER["latitude_of_origin",40.16666666666666],PARAMETER["central_meridian",-74],PARAMETER["false_easting",984250],PARAMETER["false_northing",0],UNIT["Foot_US",0.3048006096012192]]
diff --git a/geopandas/tests/data/overlay/nybb_qgis/qgis-symmetric_difference.shp b/geopandas/tests/data/overlay/nybb_qgis/qgis-symmetric_difference.shp
new file mode 100644
Binary files /dev/null and b/geopandas/tests/data/overlay/nybb_qgis/qgis-symmetric_difference.shp differ
diff --git a/geopandas/tests/data/overlay/nybb_qgis/qgis-symmetric_difference.shx b/geopandas/tests/data/overlay/nybb_qgis/qgis-symmetric_difference.shx
new file mode 100644
Binary files /dev/null and b/geopandas/tests/data/overlay/nybb_qgis/qgis-symmetric_difference.shx differ
diff --git a/geopandas/tests/data/overlay/nybb_qgis/qgis-union.dbf b/geopandas/tests/data/overlay/nybb_qgis/qgis-union.dbf
new file mode 100644
Binary files /dev/null and b/geopandas/tests/data/overlay/nybb_qgis/qgis-union.dbf differ
diff --git a/geopandas/tests/data/overlay/nybb_qgis/qgis-union.prj b/geopandas/tests/data/overlay/nybb_qgis/qgis-union.prj
new file mode 100644
--- /dev/null
+++ b/geopandas/tests/data/overlay/nybb_qgis/qgis-union.prj
@@ -0,0 +1 @@
+PROJCS["Lambert_Conformal_Conic",GEOGCS["GCS_GRS 1980(IUGG, 1980)",DATUM["D_unknown",SPHEROID["GRS80",6378137,298.257222101]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]],PROJECTION["Lambert_Conformal_Conic"],PARAMETER["standard_parallel_1",40.66666666666666],PARAMETER["standard_parallel_2",41.03333333333333],PARAMETER["latitude_of_origin",40.16666666666666],PARAMETER["central_meridian",-74],PARAMETER["false_easting",984250],PARAMETER["false_northing",0],UNIT["Foot_US",0.30480060960121924]]
\ No newline at end of file
diff --git a/geopandas/tests/data/overlay/nybb_qgis/qgis-union.qpj b/geopandas/tests/data/overlay/nybb_qgis/qgis-union.qpj
new file mode 100644
--- /dev/null
+++ b/geopandas/tests/data/overlay/nybb_qgis/qgis-union.qpj
@@ -0,0 +1 @@
+PROJCS["unnamed",GEOGCS["GRS 1980(IUGG, 1980)",DATUM["unknown",SPHEROID["GRS80",6378137,298.257222101],TOWGS84[0,0,0,0,0,0,0]],PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433]],PROJECTION["Lambert_Conformal_Conic_2SP"],PARAMETER["standard_parallel_1",40.66666666666666],PARAMETER["standard_parallel_2",41.03333333333333],PARAMETER["latitude_of_origin",40.16666666666666],PARAMETER["central_meridian",-74],PARAMETER["false_easting",984250],PARAMETER["false_northing",0],UNIT["Foot_US",0.3048006096012192]]
diff --git a/geopandas/tests/data/overlay/nybb_qgis/qgis-union.shp b/geopandas/tests/data/overlay/nybb_qgis/qgis-union.shp
new file mode 100644
Binary files /dev/null and b/geopandas/tests/data/overlay/nybb_qgis/qgis-union.shp differ
diff --git a/geopandas/tests/data/overlay/nybb_qgis/qgis-union.shx b/geopandas/tests/data/overlay/nybb_qgis/qgis-union.shx
new file mode 100644
Binary files /dev/null and b/geopandas/tests/data/overlay/nybb_qgis/qgis-union.shx differ
diff --git a/geopandas/tests/data/overlay/overlap/df1_df2_overlap-difference.geojson b/geopandas/tests/data/overlay/overlap/df1_df2_overlap-difference.geojson
new file mode 100644
--- /dev/null
+++ b/geopandas/tests/data/overlay/overlap/df1_df2_overlap-difference.geojson
@@ -0,0 +1,9 @@
+{
+"type": "FeatureCollection",
+"crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } },
+                                                                                
+"features": [
+{ "type": "Feature", "properties": { "col1": 1 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 1.0, 0.0 ], [ 0.707106781186548, -0.707106781186547 ], [ 0.0, -1.0 ], [ -0.707106781186546, -0.707106781186549 ], [ -1.0, -0.0 ], [ -0.70710678118655, 0.707106781186545 ], [ -0.0, 1.0 ], [ 0.292893218813454, 0.292893218813451 ], [ 1.0, 0.0 ] ] ] } },
+{ "type": "Feature", "properties": { "col1": 2 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 2.5, 0.0 ], [ 2.207106781186548, -0.707106781186547 ], [ 1.5, -1.0 ], [ 0.792893218813454, -0.707106781186549 ], [ 0.5, -0.0 ], [ 0.573223304703363, 0.176776695296637 ], [ 1.0, 0.0 ], [ 1.707106781186548, 0.292893218813453 ], [ 1.926776695296637, 0.823223304703364 ], [ 2.207106781186543, 0.707106781186551 ], [ 2.5, 0.0 ] ] ] } }
+]
+}
diff --git a/geopandas/tests/data/overlay/overlap/df1_df2_overlap-intersection.geojson b/geopandas/tests/data/overlay/overlap/df1_df2_overlap-intersection.geojson
new file mode 100644
--- /dev/null
+++ b/geopandas/tests/data/overlay/overlap/df1_df2_overlap-intersection.geojson
@@ -0,0 +1,9 @@
+{
+"type": "FeatureCollection",
+"crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } },
+                                                                                
+"features": [
+{ "type": "Feature", "properties": { "col1": 1, "col2": 1 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -0.0, 1.0 ], [ 0.707106781186544, 0.707106781186551 ], [ 1.0, 0.0 ], [ 0.292893218813454, 0.292893218813451 ], [ -0.0, 1.0 ] ] ] } },
+{ "type": "Feature", "properties": { "col1": 2, "col2": 1 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 0.573223304703363, 0.176776695296637 ], [ 0.79289321881345, 0.707106781186545 ], [ 1.5, 1.0 ], [ 1.926776695296637, 0.823223304703364 ], [ 1.707106781186548, 0.292893218813453 ], [ 1.0, 0.0 ], [ 0.573223304703363, 0.176776695296637 ] ] ] } }
+]
+}
diff --git a/geopandas/tests/data/overlay/overlap/df1_df2_overlap-symmetric_difference.geojson b/geopandas/tests/data/overlay/overlap/df1_df2_overlap-symmetric_difference.geojson
new file mode 100644
--- /dev/null
+++ b/geopandas/tests/data/overlay/overlap/df1_df2_overlap-symmetric_difference.geojson
@@ -0,0 +1,11 @@
+{
+"type": "FeatureCollection",
+"crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } },
+                                                                                
+"features": [
+{ "type": "Feature", "properties": { "col1": 1, "col2": null }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 1.0, 0.0 ], [ 0.707106781186548, -0.707106781186547 ], [ 0.0, -1.0 ], [ -0.707106781186546, -0.707106781186549 ], [ -1.0, -0.0 ], [ -0.70710678118655, 0.707106781186545 ], [ -0.0, 1.0 ], [ 0.292893218813454, 0.292893218813451 ], [ 1.0, 0.0 ] ] ] } },
+{ "type": "Feature", "properties": { "col1": 2, "col2": null }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 2.5, 0.0 ], [ 2.207106781186548, -0.707106781186547 ], [ 1.5, -1.0 ], [ 0.792893218813454, -0.707106781186549 ], [ 0.5, -0.0 ], [ 0.573223304703363, 0.176776695296637 ], [ 1.0, 0.0 ], [ 1.707106781186548, 0.292893218813453 ], [ 1.926776695296637, 0.823223304703364 ], [ 2.207106781186543, 0.707106781186551 ], [ 2.5, 0.0 ] ] ] } },
+{ "type": "Feature", "properties": { "col1": null, "col2": 1 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 2.0, 1.0 ], [ 1.926776695296637, 0.823223304703364 ], [ 1.5, 1.0 ], [ 0.79289321881345, 0.707106781186545 ], [ 0.75, 0.603553390593273 ], [ 0.707106781186544, 0.707106781186551 ], [ -0.0, 1.0 ], [ 0.29289321881345, 1.707106781186545 ], [ 1.0, 2.0 ], [ 1.707106781186543, 1.707106781186551 ], [ 2.0, 1.0 ] ] ] } },
+{ "type": "Feature", "properties": { "col1": null, "col2": 2 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 3.0, 2.0 ], [ 2.707106781186548, 1.292893218813453 ], [ 2.0, 1.0 ], [ 1.292893218813454, 1.292893218813451 ], [ 1.0, 2.0 ], [ 1.292893218813449, 2.707106781186544 ], [ 2.0, 3.0 ], [ 2.707106781186543, 2.707106781186551 ], [ 3.0, 2.0 ] ] ] } }
+]
+}
diff --git a/geopandas/tests/data/overlay/overlap/df1_df2_overlap-union.geojson b/geopandas/tests/data/overlay/overlap/df1_df2_overlap-union.geojson
new file mode 100644
--- /dev/null
+++ b/geopandas/tests/data/overlay/overlap/df1_df2_overlap-union.geojson
@@ -0,0 +1,14 @@
+{
+"type": "FeatureCollection",
+"crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } },
+                                                                                
+"features": [
+{ "type": "Feature", "properties": { "col1": 1, "col2": 1 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -0.0, 1.0 ], [ 0.707106781186544, 0.707106781186551 ], [ 1.0, 0.0 ], [ 0.292893218813454, 0.292893218813451 ], [ -0.0, 1.0 ] ] ] } },
+{ "type": "Feature", "properties": { "col1": 1, "col2": null }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 1.0, 0.0 ], [ 0.707106781186548, -0.707106781186547 ], [ 0.0, -1.0 ], [ -0.707106781186546, -0.707106781186549 ], [ -1.0, -0.0 ], [ -0.70710678118655, 0.707106781186545 ], [ -0.0, 1.0 ], [ 0.292893218813454, 0.292893218813451 ], [ 1.0, 0.0 ] ] ] } },
+{ "type": "Feature", "properties": { "col1": 2, "col2": 1 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 0.573223304703363, 0.176776695296637 ], [ 0.79289321881345, 0.707106781186545 ], [ 1.5, 1.0 ], [ 1.926776695296637, 0.823223304703364 ], [ 1.707106781186548, 0.292893218813453 ], [ 1.0, 0.0 ], [ 0.573223304703363, 0.176776695296637 ] ] ] } },
+{ "type": "Feature", "properties": { "col1": 2, "col2": null }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 2.5, 0.0 ], [ 2.207106781186548, -0.707106781186547 ], [ 1.5, -1.0 ], [ 0.792893218813454, -0.707106781186549 ], [ 0.5, -0.0 ], [ 0.573223304703363, 0.176776695296637 ], [ 1.0, 0.0 ], [ 1.707106781186548, 0.292893218813453 ], [ 1.926776695296637, 0.823223304703364 ], [ 2.207106781186543, 0.707106781186551 ], [ 2.5, 0.0 ] ] ] } },
+{ "type": "Feature", "properties": { "col1": null, "col2": 1 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 2.0, 1.0 ], [ 1.926776695296637, 0.823223304703364 ], [ 1.5, 1.0 ], [ 0.79289321881345, 0.707106781186545 ], [ 0.75, 0.603553390593273 ], [ 0.707106781186544, 0.707106781186551 ], [ -0.0, 1.0 ], [ 0.29289321881345, 1.707106781186545 ], [ 1.0, 2.0 ], [ 1.707106781186543, 1.707106781186551 ], [ 2.0, 1.0 ] ] ] } },
+{ "type": "Feature", "properties": { "col1": null, "col2": 2 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 3.0, 2.0 ], [ 2.707106781186548, 1.292893218813453 ], [ 2.0, 1.0 ], [ 1.292893218813454, 1.292893218813451 ], [ 1.0, 2.0 ], [ 1.292893218813449, 2.707106781186544 ], [ 2.0, 3.0 ], [ 2.707106781186543, 2.707106781186551 ], [ 3.0, 2.0 ] ] ] } },
+{ "type": "Feature", "properties": { "col1": null, "col2": 2 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 3.0, 2.0 ], [ 2.707106781186548, 1.292893218813453 ], [ 2.0, 1.0 ], [ 1.292893218813454, 1.292893218813451 ], [ 1.0, 2.0 ], [ 1.292893218813449, 2.707106781186544 ], [ 2.0, 3.0 ], [ 2.707106781186543, 2.707106781186551 ], [ 3.0, 2.0 ] ] ] } }
+]
+}
diff --git a/geopandas/tests/data/overlay/overlap/df1_overlap.geojson b/geopandas/tests/data/overlay/overlap/df1_overlap.geojson
new file mode 100644
--- /dev/null
+++ b/geopandas/tests/data/overlay/overlap/df1_overlap.geojson
@@ -0,0 +1,7 @@
+{
+"type": "FeatureCollection",
+"features": [
+{ "type": "Feature", "properties": { "col1": 1 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 1.0, 0.0 ], [ 0.707106781186548, -0.707106781186547 ], [ 0.0, -1.0 ], [ -0.707106781186546, -0.707106781186549 ], [ -1.0, -0.0 ], [ -0.70710678118655, 0.707106781186545 ], [ -0.0, 1.0 ], [ 0.707106781186544, 0.707106781186551 ], [ 1.0, 0.0 ] ] ] } },
+{ "type": "Feature", "properties": { "col1": 2 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 2.5, 0.0 ], [ 2.207106781186548, -0.707106781186547 ], [ 1.5, -1.0 ], [ 0.792893218813454, -0.707106781186549 ], [ 0.5, -0.0 ], [ 0.79289321881345, 0.707106781186545 ], [ 1.5, 1.0 ], [ 2.207106781186543, 0.707106781186551 ], [ 2.5, 0.0 ] ] ] } }
+]
+}
diff --git a/geopandas/tests/data/overlay/overlap/df2_overlap.geojson b/geopandas/tests/data/overlay/overlap/df2_overlap.geojson
new file mode 100644
--- /dev/null
+++ b/geopandas/tests/data/overlay/overlap/df2_overlap.geojson
@@ -0,0 +1,7 @@
+{
+"type": "FeatureCollection",
+"features": [
+{ "type": "Feature", "properties": { "col2": 1 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 2.0, 1.0 ], [ 1.707106781186548, 0.292893218813453 ], [ 1.0, 0.0 ], [ 0.292893218813454, 0.292893218813451 ], [ 0.0, 1.0 ], [ 0.29289321881345, 1.707106781186545 ], [ 1.0, 2.0 ], [ 1.707106781186543, 1.707106781186551 ], [ 2.0, 1.0 ] ] ] } },
+{ "type": "Feature", "properties": { "col2": 2 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 3.0, 2.0 ], [ 2.707106781186548, 1.292893218813453 ], [ 2.0, 1.0 ], [ 1.292893218813454, 1.292893218813451 ], [ 1.0, 2.0 ], [ 1.292893218813449, 2.707106781186544 ], [ 2.0, 3.0 ], [ 2.707106781186543, 2.707106781186551 ], [ 3.0, 2.0 ] ] ] } }
+]
+}
diff --git a/geopandas/tests/data/overlay/polys/df1.geojson b/geopandas/tests/data/overlay/polys/df1.geojson
new file mode 100644
--- /dev/null
+++ b/geopandas/tests/data/overlay/polys/df1.geojson
@@ -0,0 +1,7 @@
+{
+"type": "FeatureCollection",
+"features": [
+{ "type": "Feature", "properties": { "col1": 1 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 0.0, 0.0 ], [ 2.0, 0.0 ], [ 2.0, 2.0 ], [ 0.0, 2.0 ], [ 0.0, 0.0 ] ] ] } },
+{ "type": "Feature", "properties": { "col1": 2 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 2.0, 2.0 ], [ 4.0, 2.0 ], [ 4.0, 4.0 ], [ 2.0, 4.0 ], [ 2.0, 2.0 ] ] ] } }
+]
+}
diff --git a/geopandas/tests/data/overlay/polys/df1_df2-difference-inverse.geojson b/geopandas/tests/data/overlay/polys/df1_df2-difference-inverse.geojson
new file mode 100644
--- /dev/null
+++ b/geopandas/tests/data/overlay/polys/df1_df2-difference-inverse.geojson
@@ -0,0 +1,9 @@
+{
+"type": "FeatureCollection",
+"crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } },
+                                                                                
+"features": [
+{ "type": "Feature", "properties": { "col2": 1 }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 1.0, 2.0 ], [ 1.0, 3.0 ], [ 2.0, 3.0 ], [ 2.0, 2.0 ], [ 1.0, 2.0 ] ] ], [ [ [ 3.0, 2.0 ], [ 3.0, 1.0 ], [ 2.0, 1.0 ], [ 2.0, 2.0 ], [ 3.0, 2.0 ] ] ] ] } },
+{ "type": "Feature", "properties": { "col2": 2 }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 3.0, 4.0 ], [ 3.0, 5.0 ], [ 5.0, 5.0 ], [ 5.0, 3.0 ], [ 4.0, 3.0 ], [ 4.0, 4.0 ], [ 3.0, 4.0 ] ] ] ] } }
+]
+}
diff --git a/geopandas/tests/data/overlay/polys/df1_df2-difference.geojson b/geopandas/tests/data/overlay/polys/df1_df2-difference.geojson
new file mode 100644
--- /dev/null
+++ b/geopandas/tests/data/overlay/polys/df1_df2-difference.geojson
@@ -0,0 +1,8 @@
+{
+"type": "FeatureCollection",
+                                                                                
+"features": [
+{ "type": "Feature", "properties": { "col1": 1 }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 0.0, 0.0 ], [ 0.0, 2.0 ], [ 1.0, 2.0 ], [ 1.0, 1.0 ], [ 2.0, 1.0 ], [ 2.0, 0.0 ], [ 0.0, 0.0 ] ] ] ] } },
+{ "type": "Feature", "properties": { "col1": 2 }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 2.0, 3.0 ], [ 2.0, 4.0 ], [ 3.0, 4.0 ], [ 3.0, 3.0 ], [ 2.0, 3.0 ] ] ], [ [ [ 4.0, 3.0 ], [ 4.0, 2.0 ], [ 3.0, 2.0 ], [ 3.0, 3.0 ], [ 4.0, 3.0 ] ] ] ] } }
+]
+}
diff --git a/geopandas/tests/data/overlay/polys/df1_df2-intersection.geojson b/geopandas/tests/data/overlay/polys/df1_df2-intersection.geojson
new file mode 100644
--- /dev/null
+++ b/geopandas/tests/data/overlay/polys/df1_df2-intersection.geojson
@@ -0,0 +1,9 @@
+{
+"type": "FeatureCollection",
+                                                                                
+"features": [
+{ "type": "Feature", "properties": { "col1": 1, "col2": 1 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 1.0, 2.0 ], [ 2.0, 2.0 ], [ 2.0, 1.0 ], [ 1.0, 1.0 ], [ 1.0, 2.0 ] ] ] } },
+{ "type": "Feature", "properties": { "col1": 2, "col2": 1 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 2.0, 2.0 ], [ 2.0, 3.0 ], [ 3.0, 3.0 ], [ 3.0, 2.0 ], [ 2.0, 2.0 ] ] ] } },
+{ "type": "Feature", "properties": { "col1": 2, "col2": 2 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 3.0, 4.0 ], [ 4.0, 4.0 ], [ 4.0, 3.0 ], [ 3.0, 3.0 ], [ 3.0, 4.0 ] ] ] } }
+]
+}
diff --git a/geopandas/tests/data/overlay/polys/df1_df2-symmetric_difference.geojson b/geopandas/tests/data/overlay/polys/df1_df2-symmetric_difference.geojson
new file mode 100644
--- /dev/null
+++ b/geopandas/tests/data/overlay/polys/df1_df2-symmetric_difference.geojson
@@ -0,0 +1,10 @@
+{
+"type": "FeatureCollection",
+                                                                                
+"features": [
+{ "type": "Feature", "properties": { "col1": 1, "col2": null }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 0.0, 0.0 ], [ 0.0, 2.0 ], [ 1.0, 2.0 ], [ 1.0, 1.0 ], [ 2.0, 1.0 ], [ 2.0, 0.0 ], [ 0.0, 0.0 ] ] ] ] } },
+{ "type": "Feature", "properties": { "col1": 2, "col2": null }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 2.0, 3.0 ], [ 2.0, 4.0 ], [ 3.0, 4.0 ], [ 3.0, 3.0 ], [ 2.0, 3.0 ] ] ], [ [ [ 4.0, 3.0 ], [ 4.0, 2.0 ], [ 3.0, 2.0 ], [ 3.0, 3.0 ], [ 4.0, 3.0 ] ] ] ] } },
+{ "type": "Feature", "properties": { "col1": null, "col2": 1 }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 1.0, 2.0 ], [ 1.0, 3.0 ], [ 2.0, 3.0 ], [ 2.0, 2.0 ], [ 1.0, 2.0 ] ] ], [ [ [ 3.0, 2.0 ], [ 3.0, 1.0 ], [ 2.0, 1.0 ], [ 2.0, 2.0 ], [ 3.0, 2.0 ] ] ] ] } },
+{ "type": "Feature", "properties": { "col1": null, "col2": 2 }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 3.0, 4.0 ], [ 3.0, 5.0 ], [ 5.0, 5.0 ], [ 5.0, 3.0 ], [ 4.0, 3.0 ], [ 4.0, 4.0 ], [ 3.0, 4.0 ] ] ] ] } }
+]
+}
diff --git a/geopandas/tests/data/overlay/polys/df1_df2-union.geojson b/geopandas/tests/data/overlay/polys/df1_df2-union.geojson
new file mode 100644
--- /dev/null
+++ b/geopandas/tests/data/overlay/polys/df1_df2-union.geojson
@@ -0,0 +1,13 @@
+{
+"type": "FeatureCollection",
+                                                                                
+"features": [
+{ "type": "Feature", "properties": { "col1": 1, "col2": 1 }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 1.0, 2.0 ], [ 2.0, 2.0 ], [ 2.0, 1.0 ], [ 1.0, 1.0 ], [ 1.0, 2.0 ] ] ] ] } },
+{ "type": "Feature", "properties": { "col1": 1, "col2": null }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 0.0, 0.0 ], [ 0.0, 2.0 ], [ 1.0, 2.0 ], [ 1.0, 1.0 ], [ 2.0, 1.0 ], [ 2.0, 0.0 ], [ 0.0, 0.0 ] ] ] ] } },
+{ "type": "Feature", "properties": { "col1": 2, "col2": 1 }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 2.0, 2.0 ], [ 2.0, 3.0 ], [ 3.0, 3.0 ], [ 3.0, 2.0 ], [ 2.0, 2.0 ] ] ] ] } },
+{ "type": "Feature", "properties": { "col1": 2, "col2": 2 }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 3.0, 4.0 ], [ 4.0, 4.0 ], [ 4.0, 3.0 ], [ 3.0, 3.0 ], [ 3.0, 4.0 ] ] ] ] } },
+{ "type": "Feature", "properties": { "col1": 2, "col2": null }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 2.0, 3.0 ], [ 2.0, 4.0 ], [ 3.0, 4.0 ], [ 3.0, 3.0 ], [ 2.0, 3.0 ] ] ], [ [ [ 4.0, 3.0 ], [ 4.0, 2.0 ], [ 3.0, 2.0 ], [ 3.0, 3.0 ], [ 4.0, 3.0 ] ] ] ] } },
+{ "type": "Feature", "properties": { "col1": null, "col2": 1 }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 1.0, 2.0 ], [ 1.0, 3.0 ], [ 2.0, 3.0 ], [ 2.0, 2.0 ], [ 1.0, 2.0 ] ] ], [ [ [ 3.0, 2.0 ], [ 3.0, 1.0 ], [ 2.0, 1.0 ], [ 2.0, 2.0 ], [ 3.0, 2.0 ] ] ] ] } },
+{ "type": "Feature", "properties": { "col1": null, "col2": 2 }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 3.0, 4.0 ], [ 3.0, 5.0 ], [ 5.0, 5.0 ], [ 5.0, 3.0 ], [ 4.0, 3.0 ], [ 4.0, 4.0 ], [ 3.0, 4.0 ] ] ] ] } }
+]
+}
diff --git a/geopandas/tests/data/overlay/polys/df2.geojson b/geopandas/tests/data/overlay/polys/df2.geojson
new file mode 100644
--- /dev/null
+++ b/geopandas/tests/data/overlay/polys/df2.geojson
@@ -0,0 +1,7 @@
+{
+"type": "FeatureCollection",
+"features": [
+{ "type": "Feature", "properties": { "col2": 1 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 1.0, 1.0 ], [ 3.0, 1.0 ], [ 3.0, 3.0 ], [ 1.0, 3.0 ], [ 1.0, 1.0 ] ] ] } },
+{ "type": "Feature", "properties": { "col2": 2 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 3.0, 3.0 ], [ 5.0, 3.0 ], [ 5.0, 5.0 ], [ 3.0, 5.0 ], [ 3.0, 3.0 ] ] ] } }
+]
+}
diff --git a/geopandas/tests/test_overlay.py b/geopandas/tests/test_overlay.py
--- a/geopandas/tests/test_overlay.py
+++ b/geopandas/tests/test_overlay.py
@@ -1,133 +1,39 @@
 from __future__ import absolute_import
 
+import os
+
 import pandas as pd
 from shapely.geometry import Point, Polygon
 
 import geopandas
 from geopandas import GeoDataFrame, GeoSeries, read_file, overlay
-from geopandas.testing import assert_geodataframe_equal
+from geopandas.testing import assert_geodataframe_equal, assert_geoseries_equal
 
 import pytest
 
 
-class TestOverlayNYBB:
-
-    def setup_method(self):
-        N = 10
+DATA = os.path.join(
+    os.path.abspath(os.path.dirname(__file__)), 'data', 'overlay')
 
-        nybb_filename = geopandas.datasets.get_path('nybb')
 
-        self.polydf = read_file(nybb_filename)
-        self.crs = {'init': 'epsg:4326'}
-        b = [int(x) for x in self.polydf.total_bounds]
-        self.polydf2 = GeoDataFrame(
-            [{'geometry': Point(x, y).buffer(10000), 'value1': x + y,
-              'value2': x - y}
-             for x, y in zip(range(b[0], b[2], int((b[2]-b[0])/N)),
-                             range(b[1], b[3], int((b[3]-b[1])/N)))],
-            crs=self.crs)
-        self.pointdf = GeoDataFrame(
-            [{'geometry': Point(x, y), 'value1': x + y, 'value2': x - y}
-             for x, y in zip(range(b[0], b[2], int((b[2]-b[0])/N)),
-                             range(b[1], b[3], int((b[3]-b[1])/N)))],
-            crs=self.crs)
-
-        # TODO this appears to be necessary;
-        # why is the sindex not generated automatically?
-        self.polydf2._generate_sindex()
-
-        self.union_shape = (180, 7)
-
-    def test_union(self):
-        df = overlay(self.polydf, self.polydf2, how="union")
-        assert type(df) is GeoDataFrame
-        assert df.shape == self.union_shape
-        assert 'value1' in df.columns and 'Shape_Area' in df.columns
-
-    def test_union_no_index(self):
-        # explicitly ignore indices
-        dfB = overlay(self.polydf, self.polydf2, how="union", use_sindex=False)
-        assert dfB.shape == self.union_shape
-
-        # remove indices from df
-        self.polydf._sindex = None
-        self.polydf2._sindex = None
-        dfC = overlay(self.polydf, self.polydf2, how="union")
-        assert dfC.shape == self.union_shape
-
-    def test_union_non_numeric_index(self):
-        import string
-        letters = list(string.ascii_letters)
-
-        polydf_alpha = self.polydf.copy()
-        polydf2_alpha = self.polydf2.copy()
-        polydf_alpha.index = letters[:len(polydf_alpha)]
-        polydf2_alpha.index = letters[:len(polydf2_alpha)]
-        df = overlay(polydf_alpha, polydf2_alpha, how="union")
-        assert type(df) is GeoDataFrame
-        assert df.shape == self.union_shape
-        assert 'value1' in df.columns and 'Shape_Area' in df.columns
-
-    def test_intersection(self):
-        df = overlay(self.polydf, self.polydf2, how="intersection")
-        assert df['BoroName'][0] is not None
-        assert df.shape == (68, 7)
-
-    def test_identity(self):
-        df = overlay(self.polydf, self.polydf2, how="identity")
-        assert df.shape == (154, 7)
-
-    def test_symmetric_difference(self):
-        df = overlay(self.polydf, self.polydf2, how="symmetric_difference")
-        assert df.shape == (122, 7)
-
-    def test_difference(self):
-        df = overlay(self.polydf, self.polydf2, how="difference")
-        assert df.shape == (86, 7)
-
-    def test_bad_how(self):
-        with pytest.raises(ValueError):
-            overlay(self.polydf, self.polydf, how="spandex")
-
-    def test_nonpoly(self):
-        with pytest.raises(TypeError):
-            overlay(self.pointdf, self.polydf, how="union")
-
-    def test_duplicate_column_name(self):
-        polydf2r = self.polydf2.rename(columns={'value2': 'Shape_Area'})
-        df = overlay(self.polydf, polydf2r, how="union")
-        assert 'Shape_Area_2' in df.columns and 'Shape_Area' in df.columns
-
-    def test_geometry_not_named_geometry(self):
-        # Issue #306
-        # Add points and flip names
-        polydf3 = self.polydf.copy()
-        polydf3 = polydf3.rename(columns={'geometry': 'polygons'})
-        polydf3 = polydf3.set_geometry('polygons')
-        polydf3['geometry'] = self.pointdf.geometry.loc[0:4]
-        assert polydf3.geometry.name == 'polygons'
-
-        df = overlay(polydf3, self.polydf2, how="union")
-        assert type(df) is GeoDataFrame
-
-        df2 = overlay(self.polydf, self.polydf2, how="union")
-        assert df.geom_almost_equals(df2).all()
-
-    def test_geoseries_warning(self):
-        # Issue #305
-        with pytest.raises(NotImplementedError):
-            overlay(self.polydf, self.polydf2.geometry, how="union")
-
-
-@pytest.fixture(params=[False, True], ids=['default-index', 'string-index'])
+@pytest.fixture
 def dfs(request):
     s1 = GeoSeries([Polygon([(0, 0), (2, 0), (2, 2), (0, 2)]),
                     Polygon([(2, 2), (4, 2), (4, 4), (2, 4)])])
     s2 = GeoSeries([Polygon([(1, 1), (3, 1), (3, 3), (1, 3)]),
                     Polygon([(3, 3), (5, 3), (5, 5), (3, 5)])])
-    df1 = GeoDataFrame({'geometry': s1, 'col1': [1, 2]})
-    df2 = GeoDataFrame({'geometry': s2, 'col2': [1, 2]})
-    if request.param:
+    df1 = GeoDataFrame({'col1': [1, 2], 'geometry': s1})
+    df2 = GeoDataFrame({'col2': [1, 2], 'geometry': s2})
+    return df1, df2
+
+
+@pytest.fixture(params=['default-index', 'int-index', 'string-index'])
+def dfs_index(request, dfs):
+    df1, df2 = dfs
+    if request.param == 'int-index':
+        df1.index = [1, 2]
+        df2.index = [0, 2]
+    if request.param == 'string-index':
         df1.index = ['row1', 'row2']
     return df1, df2
 
@@ -143,108 +49,240 @@ def use_sindex(request):
     return request.param
 
 
-@pytest.fixture
-def expected_features():
-    expected = {}
-    expected['union'] = {
-        "type": "FeatureCollection",
-        "features": [
-            {"type": "Feature", "properties": { "col1": 1.0, "col2": 1.0 },
-                "geometry": { "type": "Polygon", "coordinates": [ [ [ 1.0, 2.0 ], [ 2.0, 2.0 ], [ 2.0, 1.0 ], [ 1.0, 1.0 ], [ 1.0, 2.0 ] ] ] } },
-            {"type": "Feature", "properties": { "col1": 1.0, "col2": None },
-                "geometry": { "type": "Polygon", "coordinates": [ [ [ 0.0, 0.0 ], [ 0.0, 2.0 ], [ 1.0, 2.0 ], [ 1.0, 1.0 ], [ 2.0, 1.0 ], [ 2.0, 0.0 ], [ 0.0, 0.0 ] ] ] } },
-            {"type": "Feature", "properties": { "col1": 2.0, "col2": 1.0 },
-                "geometry": { "type": "Polygon", "coordinates": [ [ [ 2.0, 2.0 ], [ 2.0, 3.0 ], [ 3.0, 3.0 ], [ 3.0, 2.0 ], [ 2.0, 2.0 ] ] ] } },
-            {"type": "Feature", "properties": { "col1": 2.0, "col2": 2.0 },
-                "geometry": { "type": "Polygon", "coordinates": [ [ [ 3.0, 4.0 ], [ 4.0, 4.0 ], [ 4.0, 3.0 ], [ 3.0, 3.0 ], [ 3.0, 4.0 ] ] ] } },
-            {"type": "Feature", "properties": { "col1": 2.0, "col2": None },
-                "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 2.0, 3.0 ], [ 2.0, 4.0 ], [ 3.0, 4.0 ], [ 3.0, 3.0 ], [ 2.0, 3.0 ] ] ], [ [ [ 4.0, 3.0 ], [ 4.0, 2.0 ], [ 3.0, 2.0 ], [ 3.0, 3.0 ], [ 4.0, 3.0 ] ] ] ] } },
-            {"type": "Feature", "properties": { "col1": None, "col2": 1.0 },
-                "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 1.0, 2.0 ], [ 1.0, 3.0 ], [ 2.0, 3.0 ], [ 2.0, 2.0 ], [ 1.0, 2.0 ] ] ], [ [ [ 3.0, 2.0 ], [ 3.0, 1.0 ], [ 2.0, 1.0 ], [ 2.0, 2.0 ], [ 3.0, 2.0 ] ] ] ] } },
-            {"type": "Feature", "properties": { "col1": None, "col2": 2.0 },
-                "geometry": { "type": "Polygon", "coordinates": [ [ [ 3.0, 4.0 ], [ 3.0, 5.0 ], [ 5.0, 5.0 ], [ 5.0, 3.0 ], [ 4.0, 3.0 ], [ 4.0, 4.0 ], [ 3.0, 4.0 ] ] ] } }
-            ]
-        }
-
-    expected['intersection'] = {
-        "type": "FeatureCollection",
-        "features": [
-            { "type": "Feature", "properties": { "col1": 1, "col2": 1 },
-            "geometry": { "type": "Polygon", "coordinates": [ [ [ 1.0, 2.0 ], [ 2.0, 2.0 ], [ 2.0, 1.0 ], [ 1.0, 1.0 ], [ 1.0, 2.0 ] ] ] } },
-            { "type": "Feature", "properties": { "col1": 2, "col2": 1 },
-            "geometry": { "type": "Polygon", "coordinates": [ [ [ 2.0, 2.0 ], [ 2.0, 3.0 ], [ 3.0, 3.0 ], [ 3.0, 2.0 ], [ 2.0, 2.0 ] ] ] } },
-            { "type": "Feature", "properties": { "col1": 2, "col2": 2 },
-            "geometry": { "type": "Polygon", "coordinates": [ [ [ 3.0, 4.0 ], [ 4.0, 4.0 ], [ 4.0, 3.0 ], [ 3.0, 3.0 ], [ 3.0, 4.0 ] ] ] } }
-            ]
-        }
-
-    expected['symmetric_difference'] = {
-        "type": "FeatureCollection",
-        "features": [
-            {"type": "Feature", "properties": { "col1": 1.0, "col2": None },
-                "geometry": { "type": "Polygon", "coordinates": [ [ [ 0.0, 0.0 ], [ 0.0, 2.0 ], [ 1.0, 2.0 ], [ 1.0, 1.0 ], [ 2.0, 1.0 ], [ 2.0, 0.0 ], [ 0.0, 0.0 ] ] ] } },
-            {"type": "Feature", "properties": { "col1": 2.0, "col2": None },
-                "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 2.0, 3.0 ], [ 2.0, 4.0 ], [ 3.0, 4.0 ], [ 3.0, 3.0 ], [ 2.0, 3.0 ] ] ], [ [ [ 4.0, 3.0 ], [ 4.0, 2.0 ], [ 3.0, 2.0 ], [ 3.0, 3.0 ], [ 4.0, 3.0 ] ] ] ] } },
-            {"type": "Feature", "properties": { "col1": None, "col2": 1.0 },
-                "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 1.0, 2.0 ], [ 1.0, 3.0 ], [ 2.0, 3.0 ], [ 2.0, 2.0 ], [ 1.0, 2.0 ] ] ], [ [ [ 3.0, 2.0 ], [ 3.0, 1.0 ], [ 2.0, 1.0 ], [ 2.0, 2.0 ], [ 3.0, 2.0 ] ] ] ] } },
-            {"type": "Feature", "properties": { "col1": None, "col2": 2.0 },
-                "geometry": { "type": "Polygon", "coordinates": [ [ [ 3.0, 4.0 ], [ 3.0, 5.0 ], [ 5.0, 5.0 ], [ 5.0, 3.0 ], [ 4.0, 3.0 ], [ 4.0, 4.0 ], [ 3.0, 4.0 ] ] ] } }
-            ]
-        }
-
-    expected['difference'] = {
-        "type": "FeatureCollection",
-        "features": [
-            {"type": "Feature", "properties": { "col1": 1 },
-                "geometry": { "type": "Polygon", "coordinates": [ [ [ 0.0, 0.0 ], [ 0.0, 2.0 ], [ 1.0, 2.0 ], [ 1.0, 1.0 ], [ 2.0, 1.0 ], [ 2.0, 0.0 ], [ 0.0, 0.0 ] ] ] } },
-            {"type": "Feature", "properties": { "col1": 2 },
-                "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 2.0, 3.0 ], [ 2.0, 4.0 ], [ 3.0, 4.0 ], [ 3.0, 3.0 ], [ 2.0, 3.0 ] ] ], [ [ [ 4.0, 3.0 ], [ 4.0, 2.0 ], [ 3.0, 2.0 ], [ 3.0, 3.0 ], [ 4.0, 3.0 ] ] ] ] } }
-            ]
-        }
-
-    expected['difference_inverse'] = {
-        "type": "FeatureCollection",
-        "features": [
-            {"type": "Feature", "properties": { "col2": 1 },
-                "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 1.0, 2.0 ], [ 1.0, 3.0 ], [ 2.0, 3.0 ], [ 2.0, 2.0 ], [ 1.0, 2.0 ] ] ], [ [ [ 3.0, 2.0 ], [ 3.0, 1.0 ], [ 2.0, 1.0 ], [ 2.0, 2.0 ], [ 3.0, 2.0 ] ] ] ] } },
-            {"type": "Feature", "properties": { "col2": 2 },
-                "geometry": { "type": "Polygon", "coordinates": [ [ [ 3.0, 4.0 ], [ 3.0, 5.0 ], [ 5.0, 5.0 ], [ 5.0, 3.0 ], [ 4.0, 3.0 ], [ 4.0, 4.0 ], [ 3.0, 4.0 ] ] ] } }
-            ]
-        }
-
-    return expected
-
-
-@pytest.mark.skip(reason="overlay not correctly implemented")
-def test_overlay(dfs, how, use_sindex, expected_features):
+def test_overlay(dfs_index, how, use_sindex):
     """
     Basic overlay test with small dummy example dataframes (from docs).
     Results obtained using QGIS 2.16 (Vector -> Geoprocessing Tools ->
-    Intersection / Union / ...), saved to GeoJSON and pasted here
+    Intersection / Union / ...), saved to GeoJSON
     """
-    df1, df2 = dfs
+    df1, df2 = dfs_index
     result = overlay(df1, df2, how=how, use_sindex=use_sindex)
 
     # construction of result
+
+    def _read(name):
+        expected = read_file(
+            os.path.join(DATA, 'polys', 'df1_df2-{0}.geojson'.format(name)))
+        expected.crs = None
+        return expected
+
     if how == 'identity':
+        expected_intersection = _read('intersection')
+        expected_difference = _read('difference')
         expected = pd.concat([
-            GeoDataFrame.from_features(expected_features['intersection']),
-            GeoDataFrame.from_features(expected_features['difference'])
+            expected_intersection,
+            expected_difference
         ], ignore_index=True)
     else:
-        expected = GeoDataFrame.from_features(expected_features[how])
+        expected = _read(how)
 
     # TODO needed adaptations to result
-    # if how == 'union':
-    #     result = result.drop(['idx1', 'idx2'], axis=1).sort_values(['col1', 'col2']).reset_index(drop=True)
-    # elif how in ('intersection', 'identity'):
-    #     result = result.drop(['idx1', 'idx2'], axis=1)
+    if how == 'union':
+        result = result.sort_values(['col1', 'col2']).reset_index(drop=True)
+    elif how == 'difference':
+        result = result.reset_index(drop=True)
 
-    assert_geodataframe_equal(result, expected)
+    assert_geodataframe_equal(result, expected, check_column_type=False)
 
     # for difference also reversed
     if how == 'difference':
         result = overlay(df2, df1, how=how, use_sindex=use_sindex)
-        expected = GeoDataFrame.from_features(
-            expected_features['difference_inverse'])
-        assert_geodataframe_equal(result, expected)
+        result = result.reset_index(drop=True)
+        expected = _read('difference-inverse')
+        assert_geodataframe_equal(result, expected, check_column_type=False)
+
+
+def test_overlay_nybb(how):
+    polydf = read_file(geopandas.datasets.get_path('nybb'))
+
+    # construct circles dataframe
+    N = 10
+    b = [int(x) for x in polydf.total_bounds]
+    polydf2 = GeoDataFrame(
+            [{'geometry': Point(x, y).buffer(10000), 'value1': x + y,
+              'value2': x - y}
+             for x, y in zip(range(b[0], b[2], int((b[2]-b[0])/N)),
+                             range(b[1], b[3], int((b[3]-b[1])/N)))],
+            crs=polydf.crs)
+
+    result = overlay(polydf, polydf2, how=how)
+
+    cols = ['BoroCode', 'BoroName', 'Shape_Leng', 'Shape_Area',
+            'value1', 'value2']
+    if how == 'difference':
+        cols = cols[:-2]
+
+    # expected result
+
+    if how == 'identity':
+        # read union one, further down below we take the appropriate subset
+        expected = read_file(os.path.join(
+            DATA, 'nybb_qgis', 'qgis-union.shp'))
+    else:
+        expected = read_file(os.path.join(
+            DATA, 'nybb_qgis', 'qgis-{0}.shp'.format(how)))
+
+    # The result of QGIS for 'union' contains incorrect geometries:
+    # 24 is a full original circle overlapping with unioned geometries, and
+    # 27 is a completely duplicated row)
+    if how == 'union':
+        expected = expected.drop([24, 27])
+        expected.reset_index(inplace=True, drop=True)
+    # Eliminate observations without geometries (issue from QGIS)
+    expected = expected[expected.is_valid]
+    expected.reset_index(inplace=True, drop=True)
+
+    if how == 'identity':
+        expected = expected[expected.BoroCode.notnull()].copy()
+
+    # Order GeoDataFrames
+    expected = expected.sort_values(cols).reset_index(drop=True)
+
+    # TODO needed adaptations to result
+    result = result.sort_values(cols).reset_index(drop=True)
+
+    if how in ('union', 'identity'):
+        # concat < 0.23 sorts, so changes the order of the columns
+        # but at least we ensure 'geometry' is the last column
+        assert result.columns[-1] == 'geometry'
+        assert len(result.columns) == len(expected.columns)
+        result = result.reindex(columns=expected.columns)
+
+    assert_geodataframe_equal(result, expected, check_crs=False,
+                              check_column_type=False,)
+
+
+def test_overlay_overlap(how):
+    """
+    Overlay test with overlapping geometries in both dataframes.
+    Test files are created with::
+
+        import geopandas
+        from geopandas import GeoSeries, GeoDataFrame
+        from shapely.geometry import Point, Polygon, LineString
+
+        s1 = GeoSeries([Point(0, 0), Point(1.5, 0)]).buffer(1, resolution=2)
+        s2 = GeoSeries([Point(1, 1), Point(2, 2)]).buffer(1, resolution=2)
+
+        df1 = GeoDataFrame({'geometry': s1, 'col1':[1,2]})
+        df2 = GeoDataFrame({'geometry': s2, 'col2':[1, 2]})
+
+        ax = df1.plot(alpha=0.5)
+        df2.plot(alpha=0.5, ax=ax, color='C1')
+
+        df1.to_file('geopandas/geopandas/tests/data/df1_overlap.geojson',
+                    driver='GeoJSON')
+        df2.to_file('geopandas/geopandas/tests/data/df2_overlap.geojson',
+                    driver='GeoJSON')
+
+    and then overlay results are obtained from using  QGIS 2.16
+    (Vector -> Geoprocessing Tools -> Intersection / Union / ...),
+    saved to GeoJSON.
+    """
+    df1 = read_file(os.path.join(DATA, 'overlap', 'df1_overlap.geojson'))
+    df2 = read_file(os.path.join(DATA, 'overlap', 'df2_overlap.geojson'))
+
+    result = overlay(df1, df2, how=how)
+
+    if how == 'identity':
+        raise pytest.skip()
+
+    expected = read_file(os.path.join(
+        DATA, 'overlap', 'df1_df2_overlap-{0}.geojson'.format(how)))
+
+    if how == 'union':
+        # the QGIS result has the last row duplicated, so removing this
+        expected = expected.iloc[:-1]
+
+    # TODO needed adaptations to result
+    result = result.reset_index(drop=True)
+    if how == 'union':
+        result = result.sort_values(['col1', 'col2']).reset_index(drop=True)
+
+    assert_geodataframe_equal(result, expected, check_column_type=False,
+                              check_less_precise=True)
+
+
+@pytest.mark.parametrize('other_geometry', [False, True])
+def test_geometry_not_named_geometry(dfs, how, other_geometry):
+    # Issue #306
+    # Add points and flip names
+    df1, df2 = dfs
+    df3 = df1.copy()
+    df3 = df3.rename(columns={'geometry': 'polygons'})
+    df3 = df3.set_geometry('polygons')
+    if other_geometry:
+        df3['geometry'] = df1.centroid.geometry
+    assert df3.geometry.name == 'polygons'
+
+    res1 = overlay(df1, df2, how=how)
+    res2 = overlay(df3, df2, how=how)
+
+    assert df3.geometry.name == 'polygons'
+
+    if how == 'difference':
+        # in case of 'difference', column names of left frame are preserved
+        assert res2.geometry.name == 'polygons'
+        if other_geometry:
+            assert 'geometry' in res2.columns
+            assert_geoseries_equal(res2['geometry'], df3['geometry'],
+                                   check_series_type=False)
+            res2 = res2.drop(['geometry'], axis=1)
+        res2 = res2.rename(columns={'polygons': 'geometry'})
+        res2 = res2.set_geometry('geometry')
+
+    # TODO if existing column is overwritten -> geometry not last column
+    if other_geometry and how == 'intersection':
+        res2 = res2.reindex(columns=res1.columns)
+    assert_geodataframe_equal(res1, res2)
+
+    df4 = df2.copy()
+    df4 = df4.rename(columns={'geometry': 'geom'})
+    df4 = df4.set_geometry('geom')
+    if other_geometry:
+        df4['geometry'] = df2.centroid.geometry
+    assert df4.geometry.name == 'geom'
+
+    res1 = overlay(df1, df2, how=how)
+    res2 = overlay(df1, df4, how=how)
+    assert_geodataframe_equal(res1, res2)
+
+
+def test_bad_how(dfs):
+    df1, df2 = dfs
+    with pytest.raises(ValueError):
+        overlay(df1, df2, how="spandex")
+
+
+def test_raise_nonpoly(dfs):
+    polydf, _ = dfs
+    pointdf = polydf.copy()
+    pointdf['geometry'] = pointdf.geometry.centroid
+
+    with pytest.raises(TypeError):
+        overlay(pointdf, polydf, how="union")
+
+
+def test_duplicate_column_name(dfs):
+    df1, df2 = dfs
+    df2r = df2.rename(columns={'col2': 'col1'})
+    res = overlay(df1, df2r, how="union")
+    assert ('col1_1' in res.columns) and ('col1_2' in res.columns)
+
+
+def test_geoseries_warning(dfs):
+    df1, df2 = dfs
+    # Issue #305
+    with pytest.raises(NotImplementedError):
+        overlay(df1, df2.geometry, how="union")
+
+
+def test_preserve_crs(dfs, how):
+    df1, df2 = dfs
+    result = overlay(df1, df2, how=how)
+    assert result.crs is None
+    crs = {'init': 'epsg:4326'}
+    df1.crs = crs
+    df2.crs = crs
+    result = overlay(df1, df2, how=how)
+    assert result.crs == crs