Repository: geopandas/geopandas

Issue:
[BUG] sjoin does not check for float indexes when creating RTree index
This is continuation from bug 351 (https://github.com/geopandas/geopandas/issues/351). When bug 351 is fixed by indexing using `.loc`, a second bug is uncovered because RTree index does not accept float as the first argument, as demonstrated below:

```
gdf = gpd.GeoDataFrame([[tuple(sorted(l)) for l in np.random.rand(10, 4)]], 
                       crs='epsg:4326', index=['geometry'],
                       columns=np.arange(0,1.0,0.1)).transpose()

idx = rtree.index.Index()
for i in gdf.index:
    idx.insert(i, gdf['geometry'].loc[i])
```

Which raises an ArgumentError:

```
---------------------------------------------------------------------------
ArgumentError                             Traceback (most recent call last)
<ipython-input-18-7ff06308e75e> in <module>()
      5 idx = rtree.index.Index()
      6 for i in gdf.index:
----> 7     idx.insert(i, gdf['geometry'].loc[i])

C:\Miniconda3\lib\site-packages\rtree\index.py in insert(self, id, coordinates, obj)
    364         if obj is not None:
    365             size, data, pyserialized = self._serialize(obj)
--> 366         core.rt.Index_InsertData(self.handle, id, p_mins, p_maxs, self.properties.dimension, data, size)
    367     add = insert
    368 

ArgumentError: argument 2: <class 'TypeError'>: wrong type
```

For my present case, it can be fixed by simply typecasting to `int`, like so:

```
gdf = gpd.GeoDataFrame([[tuple(sorted(l)) for l in np.random.rand(10, 4)]], 
                       crs='epsg:4326', index=['geometry'],
                       columns=np.arange(0,1.0,0.1)).transpose()

idx = rtree.index.Index()
for i in gdf.index:
    idx.insert(int(i), gdf['geometry'].loc[i])
```

However, there may be users with use cases that cannot accept such typecasting.

I am using Pandas 0.18.1.

**UPDATE**
If the GeoDataFrame index is integer, rather than float, sjoin does not raise any errors. Users need to be warned if index is not integer.



Discussion:
@dragonator4 Thanks for reporting!

Can you make an example with `sjoin` that gives this error (or the error from the other issue)? That makes it a bit easier to test.

```python
gpd.sjoin(gpd.GeoDataFrame(geometry=[shapely.geometry.Point(0, 0)], index=[0.1]), 
          gpd.GeoDataFrame(geometry=[shapely.geometry.Point(1, 1)], index=[0.2]))
```

Resolving patch:
diff --git a/geopandas/tools/sjoin.py b/geopandas/tools/sjoin.py
--- a/geopandas/tools/sjoin.py
+++ b/geopandas/tools/sjoin.py
@@ -38,24 +38,42 @@ def sjoin(left_df, right_df, how='inner', op='intersects',
         raise ValueError("`op` was \"%s\" but is expected to be in %s" % \
             (op, allowed_ops))
 
+    if left_df.crs != right_df.crs:
+        print('Warning: CRS does not match!')
+
+    index_left = 'index_%s' % lsuffix
+    index_right = 'index_%s' % rsuffix
+
+    # due to GH 352
+    if (any(left_df.columns.isin([index_left, index_right]))
+            or any(right_df.columns.isin([index_left, index_right]))):
+        raise ValueError("'{0}' and '{1}' cannot be names in the frames being"
+                         " joined".format(index_left, index_right))
+
+    # the rtree spatial index only allows limited (numeric) index types, but an
+    # index in geopandas may be any arbitrary dtype. so reset both indices now
+    # and store references to the original indices, to be reaffixed later.
+    # GH 352
+    left_df.index.name = index_left
+    right_df.index.name = index_right
+    left_df = left_df.reset_index()
+    right_df = right_df.reset_index()
+
     if op == "within":
         # within implemented as the inverse of contains; swap names
         left_df, right_df = right_df, left_df
 
-    if left_df.crs != right_df.crs:
-        print('Warning: CRS does not match!')
-
-    tree_idx = rtree.index.Index()
+    # insert the bounds in the rtree spatial index
     right_df_bounds = right_df.geometry.apply(lambda x: x.bounds)
-    for i in right_df_bounds.index:
-        tree_idx.insert(i, right_df_bounds[i])
+    stream = ((i, b, None) for i, b in enumerate(right_df_bounds))
+    tree_idx = rtree.index.Index(stream)
 
     idxmatch = (left_df.geometry.apply(lambda x: x.bounds)
                 .apply(lambda x: list(tree_idx.intersection(x))))
     idxmatch = idxmatch[idxmatch.apply(len) > 0]
 
     if idxmatch.shape[0] > 0:
-        # if output from  join has overlapping geometries
+        # if output from join has overlapping geometries
         r_idx = np.concatenate(idxmatch.values)
         l_idx = np.concatenate([[i] * len(v) for i, v in idxmatch.iteritems()])
 
@@ -84,7 +102,7 @@ def find_contains(a1, a2):
                            ]))
                    )
 
-        result.columns = ['index_%s' % lsuffix, 'index_%s' % rsuffix, 'match_bool']
+        result.columns = ['_key_left', '_key_right', 'match_bool']
         result = (
                   pd.DataFrame(result[result['match_bool']==1])
                   .drop('match_bool', axis=1)
@@ -92,40 +110,47 @@ def find_contains(a1, a2):
 
     else:
         # when output from the join has no overlapping geometries
-        result = pd.DataFrame(columns=['index_%s' % lsuffix, 'index_%s' % rsuffix])
+        result = pd.DataFrame(columns=['_key_left', '_key_right'])
 
     if op == "within":
         # within implemented as the inverse of contains; swap names
         left_df, right_df = right_df, left_df
-        result = result.rename(columns={
-                    'index_%s' % (lsuffix): 'index_%s' % (rsuffix),
-                    'index_%s' % (rsuffix): 'index_%s' % (lsuffix)})
+        result = result.rename(columns={'_key_left': '_key_right',
+                                        '_key_right': '_key_left'})
+
 
     if how == 'inner':
-        result = result.set_index('index_%s' % lsuffix)
-        return (
-                left_df
-                .merge(result, left_index=True, right_index=True)
-                .merge(right_df.drop(right_df.geometry.name, axis=1),
-                    left_on='index_%s' % rsuffix, right_index=True,
-                    suffixes=('_%s' % lsuffix, '_%s' % rsuffix))
-                )
+        result = result.set_index('_key_left')
+        joined = (
+                  left_df
+                  .merge(result, left_index=True, right_index=True)
+                  .merge(right_df.drop(right_df.geometry.name, axis=1),
+                      left_on='_key_right', right_index=True,
+                      suffixes=('_%s' % lsuffix, '_%s' % rsuffix))
+                 )
+        joined = joined.set_index(index_left).drop(['_key_right'], axis=1)
+        joined.index.name = None
     elif how == 'left':
-        result = result.set_index('index_%s' % lsuffix)
-        return (
-                left_df
-                .merge(result, left_index=True, right_index=True, how='left')
-                .merge(right_df.drop(right_df.geometry.name, axis=1),
-                    how='left', left_on='index_%s' % rsuffix, right_index=True,
-                    suffixes=('_%s' % lsuffix, '_%s' % rsuffix))
-                )
-    elif how == 'right':
-        return (
-                left_df
-                .drop(left_df.geometry.name, axis=1)
-                .merge(result.merge(right_df,
-                    left_on='index_%s' % rsuffix, right_index=True,
-                    how='right'), left_index=True,
-                    right_on='index_%s' % lsuffix, how='right')
-                .set_index('index_%s' % rsuffix)
-                )
+        result = result.set_index('_key_left')
+        joined = (
+                  left_df
+                  .merge(result, left_index=True, right_index=True, how='left')
+                  .merge(right_df.drop(right_df.geometry.name, axis=1),
+                      how='left', left_on='_key_right', right_index=True,
+                      suffixes=('_%s' % lsuffix, '_%s' % rsuffix))
+                 )
+        joined = joined.set_index(index_left).drop(['_key_right'], axis=1)
+        joined.index.name = None
+    else:  # how == 'right':
+        joined = (
+                  left_df
+                  .drop(left_df.geometry.name, axis=1)
+                  .merge(result.merge(right_df,
+                      left_on='_key_right', right_index=True,
+                      how='right'), left_index=True,
+                      right_on='_key_left', how='right')
+                  .set_index(index_right)
+                 )
+        joined = joined.drop(['_key_left', '_key_right'], axis=1)
+
+    return joined
\ No newline at end of file


Test patch:
diff --git a/geopandas/tools/tests/test_sjoin.py b/geopandas/tools/tests/test_sjoin.py
--- a/geopandas/tools/tests/test_sjoin.py
+++ b/geopandas/tools/tests/test_sjoin.py
@@ -13,9 +13,8 @@
 from geopandas import sjoin
 from distutils.version import LooseVersion
 
-pandas_0_16_problem = 'fails under pandas < 0.17 due to issue 251,'\
-                      'not problem with sjoin.'
-
+pandas_0_18_problem = 'fails under pandas < 0.19 due to pandas issue 15692,'\
+                        'not problem with sjoin.'
 import pytest
 
 
@@ -67,9 +66,9 @@ def dfs(request):
 
 
 @unittest.skipIf(not base.HAS_SINDEX, 'Rtree absent, skipping')
-class TestSpatialJoinNew(object):
+class TestSpatialJoin(object):
 
-    @pytest.mark.parametrize('dfs', ['default-index', pytest.mark.xfail('string-index')],
+    @pytest.mark.parametrize('dfs', ['default-index', 'string-index'],
                              indirect=True)
     @pytest.mark.parametrize('op', ['intersects', 'contains', 'within'])
     def test_inner(self, op, dfs):
@@ -89,7 +88,7 @@ def test_inner(self, op, dfs):
 
         assert_frame_equal(res, exp)
 
-    @pytest.mark.parametrize('dfs', ['default-index', pytest.mark.xfail('string-index')],
+    @pytest.mark.parametrize('dfs', ['default-index', 'string-index'],
                              indirect=True)
     @pytest.mark.parametrize('op', ['intersects', 'contains', 'within'])
     def test_left(self, op, dfs):
@@ -110,7 +109,7 @@ def test_left(self, op, dfs):
 
         assert_frame_equal(res, exp)
 
-    @pytest.mark.parametrize('dfs', ['default-index', pytest.mark.xfail('string-index')],
+    @pytest.mark.parametrize('dfs', ['default-index', 'string-index'],
                              indirect=True)
     @pytest.mark.parametrize('op', ['intersects', 'contains', 'within'])
     def test_right(self, op, dfs):
@@ -211,7 +210,8 @@ def test_sjoin_values(self):
         df = sjoin(self.polydf, self.pointdf, how='left')
         self.assertEquals(df.shape, (12,8))
 
-    @unittest.skipIf(str(pd.__version__) < LooseVersion('0.17'), pandas_0_16_problem)
+    @unittest.skipIf(str(pd.__version__) < LooseVersion('0.19'), pandas_0_18_problem)
+    @pytest.mark.xfail
     def test_no_overlapping_geometry(self):
         # Note: these tests are for correctly returning GeoDataFrame
         # when result of the join is empty
@@ -253,4 +253,4 @@ def test_no_overlapping_geometry(self):
     @unittest.skip("Not implemented")
     def test_sjoin_outer(self):
         df = sjoin(self.pointdf, self.polydf, how="outer")
-        self.assertEquals(df.shape, (21,8))
+        self.assertEquals(df.shape, (21,8))
\ No newline at end of file