Repository: geopandas/geopandas

Issue:
ENH: ability to specify varying buffer sizes
Now only a single fixed buffer size for the full GeoSeries can be specified, we could expand that to a different value for each geometry by passing a array/series of the same length.


Discussion:
Hi!

It would be nice to have this new feature!
(and I guess it's also possible to use the same approach for the `distance` argument of the `interpolate` method)

Can I assign myself for this task?
@mthh Yes, PRs are welcome!

Resolving patch:
diff --git a/geopandas/base.py b/geopandas/base.py
--- a/geopandas/base.py
+++ b/geopandas/base.py
@@ -495,14 +495,28 @@ def buffer(self, distance, resolution=16, **kwargs):
 
         Parameters
         ----------
-        distance : float
-            The radius of the buffer.
+        distance : float, np.array, pd.Series
+            The radius of the buffer. If np.array or pd.Series are used
+            then it must have same length as the GeoSeries.
         resolution: float
             Optional, the resolution of the buffer around each vertex.
         """
+        if isinstance(distance, (np.ndarray, pd.Series)):
+            if len(distance) != len(self.index):
+                raise ValueError("Length of distance sequence does not match "
+                                 "length of the GeoSeries")
+            if isinstance(distance, pd.Series):
+                if not self.index.equals(distance.index):
+                    raise ValueError("Index values of distance sequence does "
+                                     "not match index values of the GeoSeries")
+            return gpd.GeoSeries(
+                    [geom.buffer(dist, resolution, **kwargs)
+                    for geom, dist in zip(self.geometry, distance)],
+                    index=self.index, crs=self.crs)
+
         return gpd.GeoSeries([geom.buffer(distance, resolution, **kwargs)
                              for geom in self.geometry],
-                         index=self.index, crs=self.crs)
+                             index=self.index, crs=self.crs)
 
     def simplify(self, *args, **kwargs):
         """Returns a ``GeoSeries`` containing a simplified representation of
@@ -551,13 +565,27 @@ def interpolate(self, distance, normalized=False):
         Parameters
         ----------
         distance : float or Series of floats
-            Distance(s) along the geometries at which a point should be returned
+            Distance(s) along the geometries at which a point should be returned.
+            If np.array or pd.Series are used then it must have same length
+            as the GeoSeries.
         normalized : boolean
             If normalized is True, distance will be interpreted as a fraction
             of the geometric object's length.
         """
-
-        return gpd.GeoSeries([s.interpolate(distance, normalized)
+        if isinstance(distance, (np.ndarray, pd.Series)):
+            if len(distance) != len(self.index):
+                raise ValueError("Length of distance sequence does not match "
+                                 "length of the GeoSeries")
+            if isinstance(distance, pd.Series):
+                if not self.index.equals(distance.index):
+                    raise ValueError("Index values of distance sequence does "
+                                     "not match index values of the GeoSeries")
+            return gpd.GeoSeries(
+                    [s.interpolate(dist, normalized=normalized)
+                    for (s, dist) in zip(self.geometry, distance)],
+                index=self.index, crs=self.crs)
+
+        return gpd.GeoSeries([s.interpolate(distance, normalized=normalized)
                              for s in self.geometry],
             index=self.index, crs=self.crs)
 
@@ -659,8 +687,8 @@ def explode(self):
         Returns
         ------
         A GeoSeries with a MultiIndex. The levels of the MultiIndex are the
-        original index and a zero-based integer index that counts the 
-        number of single geometries within a multi-part geometry. 
+        original index and a zero-based integer index that counts the
+        number of single geometries within a multi-part geometry.
 
         Example
         -------


Test patch:
diff --git a/geopandas/tests/test_geom_methods.py b/geopandas/tests/test_geom_methods.py
--- a/geopandas/tests/test_geom_methods.py
+++ b/geopandas/tests/test_geom_methods.py
@@ -409,6 +409,25 @@ def test_interpolate(self):
         self._test_binary_topological('interpolate', expected, self.g5,
                                       1.5)
 
+    def test_interpolate_distance_array(self):
+        expected = GeoSeries([Point(0.0, 0.75), Point(1.0, 0.5)])
+        self._test_binary_topological('interpolate', expected, self.g5,
+                                      np.array([0.75, 1.5]))
+
+        expected = GeoSeries([Point(0.5, 1.0), Point(0.0, 1.0)])
+        self._test_binary_topological('interpolate', expected, self.g5,
+                                      np.array([0.75, 1.5]), normalized=True)
+
+    def test_interpolate_distance_wrong_length(self):
+        distances = np.array([1, 2, 3])
+        with pytest.raises(ValueError):
+            self.g5.interpolate(distances)
+
+    def test_interpolate_distance_wrong_length(self):
+        distances = Series([1, 2], index=[99, 98])
+        with pytest.raises(ValueError):
+            self.g5.interpolate(distances)
+
     def test_project(self):
         expected = Series([2.0, 1.5], index=self.g5.index)
         p = Point(1.0, 0.5)
@@ -486,6 +505,27 @@ def test_buffer_args(self):
             expected = original.buffer(10, **args)
             assert calculated.equals(expected)
 
+    def test_buffer_distance_array(self):
+        original = GeoSeries([self.p0, self.p0])
+        expected = GeoSeries(
+            [Polygon(((6, 5), (5, 4), (4, 5), (5, 6), (6, 5))),
+             Polygon(((10, 5), (5, 0), (0, 5), (5, 10), (10, 5))),
+             ])
+        calculated = original.buffer(np.array([1, 5]), resolution=1)
+        assert_geoseries_equal(calculated, expected, check_less_precise=True)
+
+    def test_buffer_distance_wrong_length(self):
+        original = GeoSeries([self.p0, self.p0])
+        distances = np.array([1, 2, 3])
+        with pytest.raises(ValueError):
+            original.buffer(distances)
+
+    def test_buffer_distance_wrong_index(self):
+        original = GeoSeries([self.p0, self.p0], index=[0, 1])
+        distances = Series(data=[1, 2], index=[99, 98])
+        with pytest.raises(ValueError):
+            original.buffer(distances)
+
     def test_envelope(self):
         e = self.g3.envelope
         assert np.all(e.geom_equals(self.sq))