Repository: geopandas/geopandas

Issue:
BUG: concat or merge does not preserve GeoSeries type
When combining two (or more) GeoDataFrame objects with `pd.concat`, the resulting frame is still a GeoDataFrame, but its `geometry` attribute is no longer a `GeoSeries` (with the result that although it seems a GeoDataFrame, that lots of the special geopandas methods do not work anymore):

Example:

```
In [1]: import geopandas
In [2]: from shapely.geometry import Point
In [3]: df = geopandas.GeoDataFrame({'geometry': [Point(i, i) for i in range(3)], 'values':range(3)})

In [4]: df
Out[4]:
      geometry  values
0  POINT (0 0)       0
1  POINT (1 1)       1
2  POINT (2 2)       2

In [5]: type(df)
Out[5]: geopandas.geodataframe.GeoDataFrame

In [6]: type(df.geometry)
Out[6]: geopandas.geoseries.GeoSeries

In [7]: concatted_df = pd.concat([df, df])
In [8]: concatted_df
Out[8]:
      geometry  values
0  POINT (0 0)       0
1  POINT (1 1)       1
2  POINT (2 2)       2
0  POINT (0 0)       0
1  POINT (1 1)       1
2  POINT (2 2)       2

In [9]: type(concatted_df)
Out[9]: geopandas.geodataframe.GeoDataFrame    # <---- seems OK

In [10]: type(concatted_df.geometry)
Out[10]: pandas.core.series.Series     # <--- this is not OK

In [11]: type(concatted_df['geometry'])
Out[11]: pandas.core.series.Series
```

(using geopandas master and pandas 0.16.2)



Discussion:
Maybe an easier example, directly concatting the series objects gives the wrong type:

```
In [11]: points = df.geometry

In [12]: points
Out[12]:
0    POINT (0 0)
1    POINT (1 1)
2    POINT (2 2)
Name: geometry, dtype: object

In [13]: type(points)
Out[13]: geopandas.geoseries.GeoSeries

In [14]: pd.concat([points, points])
Out[14]:
0    POINT (0 0)
1    POINT (1 1)
2    POINT (2 2)
0    POINT (0 0)
1    POINT (1 1)
2    POINT (2 2)
Name: geometry, dtype: object

In [15]: type(pd.concat([points, points]))
Out[15]: pandas.core.series.Series
```

And you get the same with `merge`:

```
In [23]: df['id'] = [1,2,3]

In [24]: normal_df = pd.DataFrame({'b': [10,20,30], 'id': [1,2,3]})

In [27]: merged = pd.merge(df, normal_df, on='id')

In [28]: merged
Out[28]:
      geometry  values  id   b
0  POINT (0 0)       0   1  10
1  POINT (1 1)       1   2  20
2  POINT (2 2)       2   3  30

In [29]: type(merged)
Out[29]: geopandas.geodataframe.GeoDataFrame

In [30]: type(merged.geometry)
Out[30]: pandas.core.series.Series
```

And there is no way to change manually the `geometry` attribute to a `GeoSeries`?

`gdf.geometry = gdf.geometry.astype(gpd.geoseries.GeoSeries)` works for me

@mattijn yes, you can always easily do something like `merged['geometry'] = GeoSeries(merged['geometry'])`, but the bug is that this should not be needed

Hmm, although my method does not seem to work, while yours does!

`gdf = geopandas.GeoDataFrame(gdf)` also works, as well as `gdf .geometry = geopandas.GeoSeries(gdf.geometry)`. It is the getitem [] way that does not work.

This seems related with #182 and #127

There is also a workaround suggested, to use the merge method instead of function:

```
In [26]: merged = pd.merge(gdf, normal_df, on='id')

In [27]: type(merged.geometry)
Out[27]: pandas.core.series.Series

In [29]: merged = gdf.merge(normal_df, on='id')

In [30]: type(merged.geometry)
Out[30]: geopandas.geoseries.GeoSeries
```

Yes, was already surprised that #182 didn't work
I tried `join` but also give same probem, while doing a `reset_index` on my `gdf` the merge on `id` works indeed


Resolving patch:
diff --git a/geopandas/geodataframe.py b/geopandas/geodataframe.py
--- a/geopandas/geodataframe.py
+++ b/geopandas/geodataframe.py
@@ -409,10 +409,18 @@ def _constructor(self):
         return GeoDataFrame
 
     def __finalize__(self, other, method=None, **kwargs):
-        """ propagate metadata from other to self """
-        # NOTE: backported from pandas master (upcoming v0.13)
-        for name in self._metadata:
-            object.__setattr__(self, name, getattr(other, name, None))
+        """propagate metadata from other to self """
+        # merge operation: using metadata of the left object
+        if method == 'merge':
+            for name in self._metadata:
+                object.__setattr__(self, name, getattr(other.left, name, None))
+        # concat operation: using metadata of the first object
+        elif method == 'concat':
+            for name in self._metadata:
+                object.__setattr__(self, name, getattr(other.objs[0], name, None))
+        else:
+            for name in self._metadata:
+                object.__setattr__(self, name, getattr(other, name, None))
         return self
 
     def copy(self, deep=True):
@@ -443,7 +451,7 @@ def plot(self, *args, **kwargs):
 
     def dissolve(self, by=None, aggfunc='first'):
         """
-        Dissolve geometries within `groupby` into single observation. 
+        Dissolve geometries within `groupby` into single observation.
 
         Parameters
         ----------
@@ -473,14 +481,14 @@ def merge_geometries(block):
             merged_geom = block.unary_union
 
             new_index = block.drop(self.geometry.name, axis=1).iloc[0][by]
-            merged_w_index = GeoSeries(merged_geom, index=Index(Series(new_index),name=by), 
+            merged_w_index = GeoSeries(merged_geom, index=Index(Series(new_index),name=by),
                                           name=self.geometry.name)
             return merged_w_index
 
 
         g = geometry.groupby(by=by, group_keys=False).apply(merge_geometries)
 
-        aggregated_geometry = GeoDataFrame(g, 
+        aggregated_geometry = GeoDataFrame(g,
                                            index=g.index,
                                            geometry=self.geometry.name)
         # Recombine


Test patch:
diff --git a/geopandas/tests/test_merge.py b/geopandas/tests/test_merge.py
new file mode 100644
--- /dev/null
+++ b/geopandas/tests/test_merge.py
@@ -0,0 +1,62 @@
+from __future__ import absolute_import
+
+import pandas as pd
+from shapely.geometry import Point
+
+from geopandas import GeoDataFrame, GeoSeries
+from geopandas.tests.util import unittest
+
+
+class TestMerging(unittest.TestCase):
+
+    def setUp(self):
+
+        self.gseries = GeoSeries([Point(i, i) for i in range(3)])
+        self.series = pd.Series([1, 2, 3])
+        self.gdf = GeoDataFrame({'geometry': self.gseries, 'values': range(3)})
+        self.df = pd.DataFrame({'col1': [1, 2, 3], 'col2': [0.1, 0.2, 0.3]})
+
+    def _check_metadata(self, gdf, geometry_column_name='geometry', crs=None):
+
+        self.assertEqual(gdf._geometry_column_name, geometry_column_name)
+        self.assertEqual(gdf.crs, crs)
+
+    def test_merge(self):
+
+        res = self.gdf.merge(self.df, left_on='values', right_on='col1')
+
+        # check result is a GeoDataFrame
+        self.assert_(isinstance(res, GeoDataFrame))
+
+        # check geometry property gives GeoSeries
+        self.assert_(isinstance(res.geometry, GeoSeries))
+
+        # check metadata
+        self._check_metadata(res)
+
+        ## test that crs and other geometry name are preserved
+        self.gdf.crs = {'init' :'epsg:4326'}
+        self.gdf = (self.gdf.rename(columns={'geometry': 'points'})
+                            .set_geometry('points'))
+        res = self.gdf.merge(self.df, left_on='values', right_on='col1')
+        self.assert_(isinstance(res, GeoDataFrame))
+        self.assert_(isinstance(res.geometry, GeoSeries))
+        self._check_metadata(res, 'points', self.gdf.crs)
+
+    def test_concat_axis0(self):
+
+        res = pd.concat([self.gdf, self.gdf])
+
+        self.assertEqual(res.shape, (6, 2))
+        self.assert_(isinstance(res, GeoDataFrame))
+        self.assert_(isinstance(res.geometry, GeoSeries))
+        self._check_metadata(res)
+
+    def test_concat_axis1(self):
+
+        res = pd.concat([self.gdf, self.df], axis=1)
+
+        self.assertEqual(res.shape, (3, 4))
+        self.assert_(isinstance(res, GeoDataFrame))
+        self.assert_(isinstance(res.geometry, GeoSeries))
+        self._check_metadata(res)