From fbb1351f10aeee67ce351f32917f67bc08a2435e Mon Sep 17 00:00:00 2001 From: Nick Eubank Date: Thu, 28 Apr 2016 16:17:04 -0700 Subject: [PATCH 1/4] initial outline --- geopandas/geodataframe.py | 55 ++++++++++++++++++++++++++++++++++++++- tests/test_dissolve.py | 40 ++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 tests/test_dissolve.py diff --git a/geopandas/geodataframe.py b/geopandas/geodataframe.py index 78ec62b..e5fdd0d 100644 --- a/geopandas/geodataframe.py +++ b/geopandas/geodataframe.py @@ -8,7 +8,7 @@ import os import sys import numpy as np -from pandas import DataFrame, Series +from pandas import DataFrame, Series, Index from shapely.geometry import mapping, shape from shapely.geometry.base import BaseGeometry from six import string_types @@ -441,6 +441,59 @@ class GeoDataFrame(GeoPandasBase, DataFrame): plot.__doc__ = plot_dataframe.__doc__ + def dissolve(self, by=None, aggfunc=None): + """ + Dissolve geometries within `groupby` into single observation. + + Parameters + ---------- + by : string, default None + Column whose values define groups to be dissolved + aggfunc : function or string, default "first" + Aggregation function for manipulation of data associated + with each group. Passed to pandas `groupby` method. + + Returns + ------- + GeoDataFrame + """ + + if aggfunc is None: + aggfunc = lambda x: x.iloc[0,:] + + # Separate data and geometry + data = self.drop(labels=self.geometry.name, axis=1).copy() + + + groupby_plus_geometry = [self.geometry.name] + groupby_plus_geometry.append(by) + geometry = self[groupby_plus_geometry].copy() + + # Process data + aggregated_data = data.groupby(by=by).apply(aggfunc).drop(by, axis=1) + + # Process geometry + 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), + name=self.geometry.name) + return merged_w_index + + + aggregated_geometry = geometry.groupby(by=by, group_keys=False) + aggregated_geometry = aggregated_geometry.apply(merge_geometries) + + aggregated_geometry = GeoDataFrame(aggregated_geometry, + index=aggregated_geometry.index, + geometry=self.geometry.name) + # Recombine + aggregated = aggregated_geometry.join(aggregated_data) + aggregated = aggregated.set_geometry(self.geometry.name) + return aggregated + def _dataframe_set_geometry(self, col, drop=False, inplace=False, crs=None): if inplace: raise ValueError("Can't do inplace setting when converting from" diff --git a/tests/test_dissolve.py b/tests/test_dissolve.py new file mode 100644 index 0000000..8bac2ba --- /dev/null +++ b/tests/test_dissolve.py @@ -0,0 +1,40 @@ +from __future__ import absolute_import +import tempfile +import shutil +from shapely.geometry import Point +from geopandas import GeoDataFrame, read_file +from geopandas.tools import overlay +from .util import unittest, download_nybb + + +class TestDataFrame(unittest.TestCase): + + def setUp(self): + + nybb_filename, nybb_zip_path = download_nybb() + self.polydf = read_file(nybb_zip_path, vfs='zip://' + nybb_filename) + + self.polydf = self.polydf.rename(columns={'geometry':'myshapes'}) + self.polydf = self.polydf.set_geometry('myshapes') + + self.polydf['manhattan_bronx'] = 0 + self.polydf.loc[3:4,'manhattan_bronx']=1 + + # Merged geometry + manhattan_bronx = self.polydf.loc[3:4,] + others = self.polydf.loc[0:2,] + + + self.merged_shapes = GeoDataFrame(columns=manhattan_bronx.columns) + self.merged_shapes.loc[0, 'myshapes'] = others.geometry.unary_union + self.merged_shapes.loc[1, 'myshapes'] = manhattan_bronx.geometry.unary_union + self.merged_shapes = self.merged_shapes.set_geometry('myshapes') + + def test_geom_dissolve(self): + test = self.polydf.dissolve('manhattan_bronx') + self.assertTrue(test.geometry.name == 'myshapes') + + known = self.merged_shapes.geometry + self.assertTrue(test.geometry.geom_almost_equals(known).all()) + + From 9d95cad7cf754fff1dfda87ddf96b4e18bf65fb5 Mon Sep 17 00:00:00 2001 From: Nick Eubank Date: Fri, 29 Apr 2016 13:47:28 -0700 Subject: [PATCH 2/4] allows strings for aggfunc --- geopandas/geodataframe.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/geopandas/geodataframe.py b/geopandas/geodataframe.py index e5fdd0d..08c8a3d 100644 --- a/geopandas/geodataframe.py +++ b/geopandas/geodataframe.py @@ -441,7 +441,7 @@ class GeoDataFrame(GeoPandasBase, DataFrame): plot.__doc__ = plot_dataframe.__doc__ - def dissolve(self, by=None, aggfunc=None): + def dissolve(self, by=None, aggfunc='first'): """ Dissolve geometries within `groupby` into single observation. @@ -458,9 +458,6 @@ class GeoDataFrame(GeoPandasBase, DataFrame): GeoDataFrame """ - if aggfunc is None: - aggfunc = lambda x: x.iloc[0,:] - # Separate data and geometry data = self.drop(labels=self.geometry.name, axis=1).copy() @@ -470,8 +467,8 @@ class GeoDataFrame(GeoPandasBase, DataFrame): geometry = self[groupby_plus_geometry].copy() # Process data - aggregated_data = data.groupby(by=by).apply(aggfunc).drop(by, axis=1) - + aggregated_data = data.groupby(by=by).agg(aggfunc) + # Process geometry def merge_geometries(block): From 88d0b86f13bcd14dca429317b4b4ff85c3d733f2 Mon Sep 17 00:00:00 2001 From: Nick Eubank Date: Mon, 2 May 2016 15:21:43 -0700 Subject: [PATCH 3/4] More tests, cleaned up --- geopandas/geodataframe.py | 7 +++---- tests/test_dissolve.py | 34 +++++++++++++++++++++++----------- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/geopandas/geodataframe.py b/geopandas/geodataframe.py index 08c8a3d..49e7691 100644 --- a/geopandas/geodataframe.py +++ b/geopandas/geodataframe.py @@ -461,10 +461,9 @@ class GeoDataFrame(GeoPandasBase, DataFrame): # Separate data and geometry data = self.drop(labels=self.geometry.name, axis=1).copy() - - groupby_plus_geometry = [self.geometry.name] - groupby_plus_geometry.append(by) - geometry = self[groupby_plus_geometry].copy() + groupby_plus_geometry_cols = [self.geometry.name] + groupby_plus_geometry_cols.append(by) + geometry = self[groupby_plus_geometry_cols].copy() # Process data aggregated_data = data.groupby(by=by).agg(aggfunc) diff --git a/tests/test_dissolve.py b/tests/test_dissolve.py index 8bac2ba..a0569d0 100644 --- a/tests/test_dissolve.py +++ b/tests/test_dissolve.py @@ -5,7 +5,8 @@ from shapely.geometry import Point from geopandas import GeoDataFrame, read_file from geopandas.tools import overlay from .util import unittest, download_nybb - +from pandas.util.testing import assert_frame_equal +from pandas import Index class TestDataFrame(unittest.TestCase): @@ -13,28 +14,39 @@ class TestDataFrame(unittest.TestCase): nybb_filename, nybb_zip_path = download_nybb() self.polydf = read_file(nybb_zip_path, vfs='zip://' + nybb_filename) + self.polydf = self.polydf[['geometry', 'BoroName', 'BoroCode']] self.polydf = self.polydf.rename(columns={'geometry':'myshapes'}) self.polydf = self.polydf.set_geometry('myshapes') - self.polydf['manhattan_bronx'] = 0 - self.polydf.loc[3:4,'manhattan_bronx']=1 + self.polydf['manhattan_bronx'] = 5 + self.polydf.loc[3:4,'manhattan_bronx']=6 # Merged geometry manhattan_bronx = self.polydf.loc[3:4,] others = self.polydf.loc[0:2,] - - self.merged_shapes = GeoDataFrame(columns=manhattan_bronx.columns) - self.merged_shapes.loc[0, 'myshapes'] = others.geometry.unary_union - self.merged_shapes.loc[1, 'myshapes'] = manhattan_bronx.geometry.unary_union - self.merged_shapes = self.merged_shapes.set_geometry('myshapes') + collapsed = [others.geometry.unary_union, manhattan_bronx.geometry.unary_union] + merged_shapes = GeoDataFrame({'myshapes': collapsed}, geometry='myshapes', + index=Index([5,6], name='manhattan_bronx')) + + # Different expected results + self.first = merged_shapes.copy() + self.first['BoroName'] = ['Staten Island', 'Manhattan'] + self.first['BoroCode'] = [5, 1] + + self.mean = merged_shapes.copy() + self.mean['BoroCode'] = [4,1.5] + def test_geom_dissolve(self): test = self.polydf.dissolve('manhattan_bronx') self.assertTrue(test.geometry.name == 'myshapes') + self.assertTrue(test.geom_almost_equals(self.first).all()) - known = self.merged_shapes.geometry - self.assertTrue(test.geometry.geom_almost_equals(known).all()) - + def test_first_dissolve(self): + test = self.polydf.dissolve('manhattan_bronx') + test = test.drop('myshapes', axis=1) + first = self.first.drop('myshapes', axis=1) + assert_frame_equal(first, test) From 38b96ac695fc071a0d03548e864ef05bff23c903 Mon Sep 17 00:00:00 2001 From: Nick Eubank Date: Tue, 3 May 2016 10:19:45 -0700 Subject: [PATCH 4/4] reorg, add more tests --- geopandas/geodataframe.py | 20 +++++++++----------- tests/test_dissolve.py | 12 ++++++++++++ 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/geopandas/geodataframe.py b/geopandas/geodataframe.py index 49e7691..ef6e071 100644 --- a/geopandas/geodataframe.py +++ b/geopandas/geodataframe.py @@ -451,24 +451,23 @@ class GeoDataFrame(GeoPandasBase, DataFrame): Column whose values define groups to be dissolved aggfunc : function or string, default "first" Aggregation function for manipulation of data associated - with each group. Passed to pandas `groupby` method. + with each group. Passed to pandas `groupby.agg` method. Returns ------- GeoDataFrame """ - # Separate data and geometry + # Process non-spatial component data = self.drop(labels=self.geometry.name, axis=1).copy() + aggregated_data = data.groupby(by=by).agg(aggfunc) + + # Process spatial component groupby_plus_geometry_cols = [self.geometry.name] groupby_plus_geometry_cols.append(by) geometry = self[groupby_plus_geometry_cols].copy() - # Process data - aggregated_data = data.groupby(by=by).agg(aggfunc) - - # Process geometry def merge_geometries(block): merged_geom = block.unary_union @@ -479,11 +478,10 @@ class GeoDataFrame(GeoPandasBase, DataFrame): return merged_w_index - aggregated_geometry = geometry.groupby(by=by, group_keys=False) - aggregated_geometry = aggregated_geometry.apply(merge_geometries) - - aggregated_geometry = GeoDataFrame(aggregated_geometry, - index=aggregated_geometry.index, + g = geometry.groupby(by=by, group_keys=False).apply(merge_geometries) + + aggregated_geometry = GeoDataFrame(g, + index=g.index, geometry=self.geometry.name) # Recombine aggregated = aggregated_geometry.join(aggregated_data) diff --git a/tests/test_dissolve.py b/tests/test_dissolve.py index a0569d0..58c91ac 100644 --- a/tests/test_dissolve.py +++ b/tests/test_dissolve.py @@ -1,6 +1,7 @@ from __future__ import absolute_import import tempfile import shutil +import numpy as np from shapely.geometry import Point from geopandas import GeoDataFrame, read_file from geopandas.tools import overlay @@ -50,3 +51,14 @@ class TestDataFrame(unittest.TestCase): first = self.first.drop('myshapes', axis=1) assert_frame_equal(first, test) + def test_mean_dissolve(self): + test = self.polydf.dissolve('manhattan_bronx', aggfunc='mean') + test = test.drop('myshapes', axis=1) + mean = self.mean.drop('myshapes', axis=1) + assert_frame_equal(mean, test) + + test = self.polydf.dissolve('manhattan_bronx', aggfunc=np.mean) + test = test.drop('myshapes', axis=1) + assert_frame_equal(mean, test) + +