Repository: geopandas/geopandas

Issue:
BUG: pd.concat([..], axis=1) fails
This "works":

```
In [86]: geopandas.__version__
Out[86]: '0.5.1'

In [87]: countries = geopandas.read_file(geopandas.datasets.get_path("naturalearth_lowres"))

In [89]: cities = geopandas.read_file(geopandas.datasets.get_path("naturalearth_cities"))

In [90]: pd.concat([cities, countries], axis=1) 
Out[90]: 
             name                                      geometry      pop_est      continent                      name iso_a3  gdp_md_est                                           geometry
0    Vatican City   POINT (12.45338654497177 41.90328217996012)     920938.0        Oceania                      Fiji    FJI      8374.0  (POLYGON ((180 -16.06713266364245, 180 -16.555...
1      San Marino     POINT (12.44177015780014 43.936095834768)   53950935.0         Africa                  Tanzania    TZA    150600.0  POLYGON ((33.90371119710453 -0.950000000000000...
2           Vaduz   POINT (9.516669472907267 47.13372377429357)     603253.0         Africa                 W. Sahara    ESH       906.5  POLYGON ((-8.665589565454809 27.65642588959236...
3      Luxembourg   POINT (6.130002806227083 49.61166037912108)   35623680.0  North America                    Canada    CAN   1674000.0  (POLYGON ((-122.84 49.00000000000011, -122.974...
4         Palikir   POINT (158.1499743237623 6.916643696007725)  326625791.0  North America  United States of America    USA  18560000.0  (POLYGON ((-122.84 49.00000000000011, -120 49....
..            ...                                           ...          ...            ...                       ...    ...         ...                                                ...
[202 rows x 8 columns]
```

But once you do something spatial with the resulting GeoDataFrame (anything that accesses the "geometry column"), things breaks (due to there being two columns with the "geometry" name).

In 0.6.0 this already started failing when doing the concat:

```
In [10]: pd.concat([cities, pd.DataFrame(countries)], axis=1)  
---------------------------------------------------------------------------
RecursionError                            Traceback (most recent call last)
<ipython-input-10-e059be43b4e0> in <module>
----> 1 pd.concat([cities, pd.DataFrame(countries)], axis=1)

~/scipy/pandas/pandas/core/reshape/concat.py in concat(objs, axis, join, join_axes, ignore_index, keys, levels, names, verify_integrity, sort, copy)
    253     )
    254 
--> 255     return op.get_result()
    256 
    257 

~/scipy/pandas/pandas/core/reshape/concat.py in get_result(self)
    474 
    475             cons = self.objs[0]._constructor
--> 476             return cons._from_axes(new_data, self.new_axes).__finalize__(
    477                 self, method="concat"
    478             )

~/scipy/pandas/pandas/core/generic.py in _from_axes(cls, data, axes, **kwargs)
    407         # for construction from BlockManager
    408         if isinstance(data, BlockManager):
--> 409             return cls(data, **kwargs)
    410         else:
    411             if cls._AXIS_REVERSED:

~/scipy/geopandas/geopandas/geodataframe.py in __init__(self, *args, **kwargs)
     74             index = self.index
     75             try:
---> 76                 self["geometry"] = _ensure_geometry(self["geometry"].values)
     77             except TypeError:
     78                 pass

~/scipy/geopandas/geopandas/geodataframe.py in __getitem__(self, key)
    554         GeoDataFrame.
    555         """
--> 556         result = super(GeoDataFrame, self).__getitem__(key)
    557         geo_col = self._geometry_column_name
    558         if isinstance(key, str) and key == geo_col:

~/scipy/pandas/pandas/core/frame.py in __getitem__(self, key)
   2693             indexer = np.where(indexer)[0]
   2694 
-> 2695         data = self.take(indexer, axis=1)
   2696 
   2697         if is_single_key:

~/scipy/pandas/pandas/core/generic.py in take(self, indices, axis, is_copy, **kwargs)
   3432             indices, axis=self._get_block_manager_axis(axis), verify=True
   3433         )
-> 3434         result = self._constructor(new_data).__finalize__(self)
   3435 
   3436         # Maybe set copy if we didn't actually change the index.

... last 4 frames repeated, from the frame below ...

~/scipy/geopandas/geopandas/geodataframe.py in __init__(self, *args, **kwargs)
     74             index = self.index
     75             try:
---> 76                 self["geometry"] = _ensure_geometry(self["geometry"].values)
     77             except TypeError:
     78                 pass

RecursionError: maximum recursion depth exceeded
```
```


Discussion:
This appears to be causing plotnine to break on 0.6.* versions. For example...

```python
import geopandas
from plotnine import *

ne = geopandas.read_file(geopandas.datasets.get_path("naturalearth_lowres"))
ggplot() + geom_map(ne)
```

Raises: AttributeError: 'GeometryArray' object has no attribute 'view'

Stacktrace

<details>

~/.virtualenvs/tidytuesday/lib/python3.6/site-packages/plotnine/ggplot.py in __repr__(self)
     86         # in the jupyter notebook.
     87         if not self.figure:
---> 88             self.draw()
     89         plt.show()
     90         return '<ggplot: (%d)>' % self.__hash__()

~/.virtualenvs/tidytuesday/lib/python3.6/site-packages/plotnine/ggplot.py in draw(self, return_ggplot)
    179         # new frames knowing that they are separate from the original.
    180         with pd.option_context('mode.chained_assignment', None):
--> 181             return self._draw(return_ggplot)
    182 
    183     def _draw(self, return_ggplot=False):

~/.virtualenvs/tidytuesday/lib/python3.6/site-packages/plotnine/ggplot.py in _draw(self, return_ggplot)
    186         # assign a default theme
    187         self = deepcopy(self)
--> 188         self._build()
    189 
    190         # If no theme we use the default

~/.virtualenvs/tidytuesday/lib/python3.6/site-packages/plotnine/ggplot.py in _build(self)
    305         # Prepare data in geoms
    306         # e.g. from y and width to ymin and ymax
--> 307         layers.setup_data()
    308 
    309         # Apply position adjustments

~/.virtualenvs/tidytuesday/lib/python3.6/site-packages/plotnine/layer.py in setup_data(self)
     70     def setup_data(self):
     71         for l in self:
---> 72             l.setup_data()
     73 
     74     def draw(self, layout, coord):

~/.virtualenvs/tidytuesday/lib/python3.6/site-packages/plotnine/layer.py in setup_data(self)
    416             return type(data)()
    417 
--> 418         data = self.geom.setup_data(data)
    419 
    420         check_required_aesthetics(

~/.virtualenvs/tidytuesday/lib/python3.6/site-packages/plotnine/geoms/geom_map.py in setup_data(self, data)
     89                 inplace=True)
     90 
---> 91         data = pd.concat([data, bounds], axis=1, copy=False)
     92         return data
     93 

~/.virtualenvs/tidytuesday/lib/python3.6/site-packages/pandas/core/reshape/concat.py in concat(objs, axis, join, join_axes, ignore_index, keys, levels, names, verify_integrity, sort, copy)
    227                        verify_integrity=verify_integrity,
    228                        copy=copy, sort=sort)
--> 229     return op.get_result()
    230 
    231 

~/.virtualenvs/tidytuesday/lib/python3.6/site-packages/pandas/core/reshape/concat.py in get_result(self)
    424             new_data = concatenate_block_managers(
    425                 mgrs_indexers, self.new_axes, concat_axis=self.axis,
--> 426                 copy=self.copy)
    427             if not self.copy:
    428                 new_data._consolidate_inplace()

~/.virtualenvs/tidytuesday/lib/python3.6/site-packages/pandas/core/internals/managers.py in concatenate_block_managers(mgrs_indexers, axes, concat_axis, copy)
   2052                 values = values.copy()
   2053             elif not copy:
-> 2054                 values = values.view()
   2055             b = b.make_block_same_class(values, placement=placement)
   2056         elif is_uniform_join_units(join_units):

AttributeError: 'GeometryArray' object has no attribute 'view'
</details>

since Geopandas allows only one geometry column to be specified, I was wondering as to what the ideal solution be for this? I have been working around it by converting it to a Dataframe
I'd like to work on fixing this. As far as I can see, there are two options
1. Raise an exception when concat is called with arguments where the geometry column is not unique
2. Mangle the repeated columns with a suffix (i.e. geometry, geometry_1, geometry_2, ...) and show a warning that these columns have been altered

Would appreciate some input on what approach is best, or if there are other alternatives. 
Having a quick look I think this would be handled in the GeoDataFrame init - and would also then catch this (more contrived) variant of the error: 
```python
from geopandas import GeoDataFrame
from shapely.geometry import Point
import pandas as pd

geoms = [Point(0, 0), Point(1, 1)]
df = pd.DataFrame({"col1": [0, 1], "geometry": geoms})
df['geometry2'] = df['geometry']
df = df.rename(columns={'geometry2': 'geometry'})

gdf = GeoDataFrame(df)
```


Resolving patch:
diff --git a/geopandas/geodataframe.py b/geopandas/geodataframe.py
--- a/geopandas/geodataframe.py
+++ b/geopandas/geodataframe.py
@@ -118,6 +118,15 @@ def __init__(self, *args, geometry=None, crs=None, **kwargs):
         # allowed in that case
         # TODO do we want to raise / return normal DataFrame in this case?
         if geometry is None and "geometry" in self.columns:
+            # Check for multiple columns with name "geometry". If there are,
+            # self["geometry"] is a gdf and constructor gets recursively recalled
+            # by pandas internals trying to access this
+            if (self.columns == "geometry").sum() > 1:
+                raise ValueError(
+                    "GeoDataFrame does not support multiple columns "
+                    "using the geometry column name 'geometry'."
+                )
+
             # only if we have actual geometry values -> call set_geometry
             index = self.index
             try:
@@ -271,11 +280,17 @@ def set_geometry(self, col, drop=False, inplace=False, crs=None):
             raise ValueError("Must pass array with one dimension only.")
         else:
             try:
-                level = frame[col].values
+                level = frame[col]
             except KeyError:
                 raise ValueError("Unknown column %s" % col)
             except Exception:
                 raise
+            if isinstance(level, DataFrame):
+                raise ValueError(
+                    "GeoDataFrame does not support setting the geometry column where "
+                    "the column name is shared by multiple columns."
+                )
+
             if drop:
                 to_remove = col
                 geo_column_name = self._geometry_column_name
@@ -1395,6 +1410,13 @@ def __finalize__(self, other, method=None, **kwargs):
             for name in self._metadata:
                 object.__setattr__(self, name, getattr(other.objs[0], name, None))
 
+            if (self.columns == self._geometry_column_name).sum() > 1:
+                raise ValueError(
+                    "Concat operation has resulted in multiple columns using "
+                    f"the geometry column name '{self._geometry_column_name}'.\n"
+                    f"Please ensure this column from the first DataFrame is not "
+                    f"repeated."
+                )
         return self
 
     def dissolve(


Test patch:
diff --git a/geopandas/tests/test_geodataframe.py b/geopandas/tests/test_geodataframe.py
--- a/geopandas/tests/test_geodataframe.py
+++ b/geopandas/tests/test_geodataframe.py
@@ -1008,6 +1008,24 @@ def test_overwrite_geometry(self):
         res = GeoDataFrame(data, geometry=geoms)
         assert_geoseries_equal(res.geometry, GeoSeries(geoms))
 
+    def test_repeat_geo_col(self):
+        df = pd.DataFrame(
+            [
+                {"geometry": Point(x, y), "geom": Point(x, y)}
+                for x, y in zip(range(3), range(3))
+            ],
+        )
+        # explicitly prevent construction of gdf with repeat geometry column names
+        # two columns called "geometry", geom col inferred
+        df2 = df.rename(columns={"geom": "geometry"})
+        with pytest.raises(ValueError):
+            GeoDataFrame(df2)
+        # ensure case is caught when custom geom column name is used
+        # two columns called "geom", geom col explicit
+        df3 = df.rename(columns={"geometry": "geom"})
+        with pytest.raises(ValueError):
+            GeoDataFrame(df3, geometry="geom")
+
 
 def test_geodataframe_crs():
     gdf = GeoDataFrame()
diff --git a/geopandas/tests/test_merge.py b/geopandas/tests/test_merge.py
--- a/geopandas/tests/test_merge.py
+++ b/geopandas/tests/test_merge.py
@@ -1,4 +1,6 @@
 import pandas as pd
+import pytest
+from geopandas.testing import assert_geodataframe_equal
 
 from shapely.geometry import Point
 
@@ -48,6 +50,13 @@ def test_concat_axis0(self):
         assert isinstance(res, GeoDataFrame)
         assert isinstance(res.geometry, GeoSeries)
         self._check_metadata(res)
+        exp = GeoDataFrame(pd.concat([pd.DataFrame(self.gdf), pd.DataFrame(self.gdf)]))
+        assert_geodataframe_equal(exp, res)
+        # check metadata comes from first gdf
+        res4 = pd.concat([self.gdf.set_crs("epsg:4326"), self.gdf], axis=0)
+        # Note: this behaviour potentially does not make sense. If geom cols are
+        # concatenated but have different CRS, then the CRS will be overridden.
+        self._check_metadata(res4, crs="epsg:4326")
 
         # series
         res = pd.concat([self.gdf.geometry, self.gdf.geometry])
@@ -63,3 +72,28 @@ def test_concat_axis1(self):
         assert isinstance(res, GeoDataFrame)
         assert isinstance(res.geometry, GeoSeries)
         self._check_metadata(res)
+
+    def test_concat_axis1_multiple_geodataframes(self):
+        # https://github.com/geopandas/geopandas/issues/1230
+        # Expect that concat should fail gracefully if duplicate column names belonging
+        # to geometry columns are introduced.
+        expected_err = (
+            "GeoDataFrame does not support multiple columns using the geometry"
+            " column name 'geometry'"
+        )
+        with pytest.raises(ValueError, match=expected_err):
+            pd.concat([self.gdf, self.gdf], axis=1)
+
+        # Check case is handled if custom geometry column name is used
+        df2 = self.gdf.rename_geometry("geom")
+        expected_err2 = (
+            "Concat operation has resulted in multiple columns using the geometry "
+            "column name 'geom'."
+        )
+        with pytest.raises(ValueError, match=expected_err2):
+            pd.concat([df2, df2], axis=1)
+
+        # Check that two geometry columns is fine, if they have different names
+        res3 = pd.concat([df2.set_crs("epsg:4326"), self.gdf], axis=1)
+        # check metadata comes from first df
+        self._check_metadata(res3, geometry_column_name="geom", crs="epsg:4326")