Source code for momepy.dimension

#!/usr/bin/env python
# -*- coding: utf-8 -*-

# dimension.py
# definitons of dimension characters

import math

import numpy as np
import pandas as pd
import scipy as sp
from shapely.geometry import LineString, Point, Polygon
from tqdm import tqdm

from .shape import _make_circle

__all__ = [
    "Area",
    "Perimeter",
    "Volume",
    "FloorArea",
    "CourtyardArea",
    "LongestAxisLength",
    "AverageCharacter",
    "StreetProfile",
    "WeightedCharacter",
    "CoveredArea",
    "PerimeterWall",
    "SegmentsLength",
]


[docs]class Area: """ Calculates area of each object in given GeoDataFrame. It can be used for any suitable element (building footprint, plot, tessellation, block). It is a simple wrapper for geopandas `gdf.geometry.area` for the consistency of momepy. Parameters ---------- gdf : GeoDataFrame GeoDataFrame containing objects to analyse Attributes ---------- series : Series Series containing resulting values gdf : GeoDataFrame original GeoDataFrame Examples -------- >>> buildings = gpd.read_file(momepy.datasets.get_path('bubenec'), layer='buildings') >>> buildings['area'] = momepy.Area(buildings).series >>> buildings.area[0] 728.5574947044363 """
[docs] def __init__(self, gdf): self.gdf = gdf self.series = self.gdf.geometry.area
[docs]class Perimeter: """ Calculates perimeter of each object in given GeoDataFrame. It can be used for any suitable element (building footprint, plot, tessellation, block). It is a simple wrapper for geopandas `gdf.geometry.length` for the consistency of momepy. Parameters ---------- gdf : GeoDataFrame GeoDataFrame containing objects to analyse Attributes ---------- series : Series Series containing resulting values gdf : GeoDataFrame original GeoDataFrame Examples -------- >>> buildings = gpd.read_file(momepy.datasets.get_path('bubenec'), layer='buildings') >>> buildings['perimeter'] = momepy.Perimeter(buildings).series >>> buildings.perimeter[0] 137.18630991119903 """
[docs] def __init__(self, gdf): self.gdf = gdf self.series = self.gdf.geometry.length
[docs]class Volume: """ Calculates volume of each object in given GeoDataFrame based on its height and area. .. math:: area * height Parameters ---------- gdf : GeoDataFrame GeoDataFrame containing objects to analyse heights : str, list, np.array, pd.Series the name of the dataframe column, np.array, or pd.Series where is stored height value areas : str, list, np.array, pd.Series (default None) the name of the dataframe column, np.array, or pd.Series where is stored area value. If set to None, function will calculate areas during the process without saving them separately. Attributes ---------- series : Series Series containing resulting values gdf : GeoDataFrame original GeoDataFrame heights : Series Series containing used heights values areas : GeoDataFrame Series containing used areas values Examples -------- >>> buildings['volume'] = momepy.Volume(buildings, heights='height_col').series >>> buildings.volume[0] 7285.5749470443625 >>> buildings['volume'] = momepy.Volume(buildings, heights='height_col', areas='area_col').series >>> buildings.volume[0] 7285.5749470443625 """
[docs] def __init__(self, gdf, heights, areas=None): self.gdf = gdf gdf = gdf.copy() if not isinstance(heights, str): gdf["mm_h"] = heights heights = "mm_h" self.heights = gdf[heights] if areas is not None: if not isinstance(areas, str): gdf["mm_a"] = areas areas = "mm_a" self.areas = gdf[areas] else: self.areas = gdf.geometry.area try: self.series = self.areas * self.heights except KeyError: raise KeyError( "ERROR: Column not found. Define heights and areas or set areas to None." )
[docs]class FloorArea: """ Calculates floor area of each object based on height and area. Number of floors is simplified into formula height / 3 (it is assumed that on average one floor is approximately 3 metres) .. math:: area * \\frac{height}{3} Parameters ---------- gdf : GeoDataFrame GeoDataFrame containing objects to analyse heights : str, list, np.array, pd.Series the name of the dataframe column, np.array, or pd.Series where is stored height value areas : str, list, np.array, pd.Series (default None) the name of the dataframe column, np.array, or pd.Series where is stored area value. If set to None, function will calculate areas during the process without saving them separately. Attributes ---------- series : Series Series containing resulting values gdf : GeoDataFrame original GeoDataFrame heights : Series Series containing used heights values areas : GeoDataFrame Series containing used areas values Examples -------- >>> buildings['floor_area'] = momepy.FloorArea(buildings, heights='height_col').series Calculating floor areas... Floor areas calculated. >>> buildings.floor_area[0] 2185.672484113309 >>> buildings['floor_area'] = momepy.FloorArea(buildings, heights='height_col', areas='area_col').series >>> buildings.floor_area[0] 2185.672484113309 """
[docs] def __init__(self, gdf, heights, areas=None): self.gdf = gdf gdf = gdf.copy() if not isinstance(heights, str): gdf["mm_h"] = heights heights = "mm_h" self.heights = gdf[heights] if areas is not None: if not isinstance(areas, str): gdf["mm_a"] = areas areas = "mm_a" self.areas = gdf[areas] else: self.areas = gdf.geometry.area try: self.series = self.areas * (self.heights // 3) except KeyError: raise KeyError( "ERROR: Column not found. Define heights and areas or set areas to None." )
[docs]class CourtyardArea: """ Calculates area of holes within geometry - area of courtyards. Ensure that your geometry is shapely Polygon. Parameters ---------- gdf : GeoDataFrame GeoDataFrame containing objects to analyse areas : str, list, np.array, pd.Series (default None) the name of the dataframe column, np.array, or pd.Series where is stored area value. If set to None, function will calculate areas during the process without saving them separately. Attributes ---------- series : Series Series containing resulting values gdf : GeoDataFrame original GeoDataFrame areas : GeoDataFrame Series containing used areas values Examples -------- >>> buildings['courtyard_area'] = momepy.CourtyardArea(buildings).series >>> buildings.courtyard_area[80] 353.33274206543274 """
[docs] def __init__(self, gdf, areas=None): self.gdf = gdf gdf = gdf.copy() if areas is None: areas = gdf.geometry.area if not isinstance(areas, str): gdf["mm_a"] = areas areas = "mm_a" self.areas = gdf[areas] self.series = gdf.apply( lambda row: Polygon(row.geometry.exterior).area - row[areas], axis=1 )
# calculate the radius of circumcircle def _longest_axis(points): circ = _make_circle(points) return circ[2] * 2
[docs]class LongestAxisLength: """ Calculates the length of the longest axis of object. Axis is defined as a diameter of minimal circumscribed circle around the convex hull. It does not have to be fully inside an object. .. math:: \\max \\left\\{d_{1}, d_{2}, \\ldots, d_{n}\\right\\} Parameters ---------- gdf : GeoDataFrame GeoDataFrame containing objects to analyse Attributes ---------- series : Series Series containing resulting values gdf : GeoDataFrame original GeoDataFrame Examples -------- >>> buildings['lal'] = momepy.LongestAxisLength(buildings).series >>> buildings.lal[0] 40.2655616057102 """
[docs] def __init__(self, gdf): self.gdf = gdf self.series = gdf.apply( lambda row: _longest_axis(row.geometry.convex_hull.exterior.coords), axis=1 )
[docs]class AverageCharacter: """ Calculates the average of a character within k steps of morphological tessellation Average value of the character within k topological steps defined in spatial_weights. Can be set to `mean`, `median` or `mode`. `mean` is defined as: .. math:: \\frac{1}{n}\\left(\\sum_{i=1}^{n} value_{i}\\right) Parameters ---------- gdf : GeoDataFrame GeoDataFrame containing morphological tessellation values : str, list, np.array, pd.Series the name of the dataframe column, np.array, or pd.Series where is stored character value. unique_id : str name of the column with unique id used as spatial_weights index. spatial_weights : libpysal.weights spatial weights matrix rng : Two-element sequence containing floats in range of [0,100], optional Percentiles over which to compute the range. Each must be between 0 and 100, inclusive. The order of the elements is not important. mode : str (default 'mean') mode of average calculation. Can be set to `mean`, `median` or `mode`. Attributes ---------- series : Series Series containing resulting values gdf : GeoDataFrame original GeoDataFrame values : GeoDataFrame Series containing used values sw : libpysal.weights spatial weights matrix id : Series Series containing used unique ID rng : tuple range mode : str mode References ---------- Hausleitner B and Berghauser Pont M (2017) Development of a configurational typology for micro-businesses integrating geometric and configurational variables. [adapted] Examples -------- >>> sw = libpysal.weights.DistanceBand.from_dataframe(tessellation, threshold=100, silence_warnings=True, ids='uID') >>> tessellation['mesh_100'] = momepy.AverageCharacter(tessellation, values='area', spatial_weights=sw, unique_id='uID').series 100%|██████████| 144/144 [00:00<00:00, 1433.32it/s] >>> tessellation.mesh_100[0] 4823.1334436678835 """
[docs] def __init__(self, gdf, values, spatial_weights, unique_id, rng=None, mode="mean"): self.gdf = gdf self.sw = spatial_weights self.id = gdf[unique_id] self.rng = rng self.mode = mode data = gdf.copy() if values is not None: if not isinstance(values, str): data["mm_v"] = values values = "mm_v" self.values = data[values] data = data.set_index(unique_id) results_list = [] for index, row in tqdm(data.iterrows(), total=data.shape[0]): neighbours = spatial_weights.neighbors[index].copy() if neighbours: neighbours.append(index) else: neighbours = [index] values_list = data.loc[neighbours][values] if rng: from momepy import limit_range values_list = limit_range(values_list, rng=rng) if mode == "mean": results_list.append(np.mean(values_list)) elif mode == "median": results_list.append(np.median(values_list)) elif mode == "mode": results_list.append(sp.stats.mode(values_list)[0][0]) else: raise ValueError("{} is not supported as mode.".format(mode)) self.series = pd.Series(results_list, index=gdf.index)
[docs]class StreetProfile: """ Calculates the street profile characters. Returns a dictionary with widths, standard deviation of width, openness, heights, standard deviation of height and ratio height/width. Algorithm generates perpendicular lines to `right` dataframe features every `distance` and measures values on intersection with features of `left.` If no feature is reached within `tick_length` its value is set as width (being theoretical maximum). .. math:: \\ Parameters ---------- left : GeoDataFrame GeoDataFrame containing streets to analyse right : GeoDataFrame GeoDataFrame containing buildings along the streets (only Polygon geometry type is supported) heights: str, list, np.array, pd.Series (default None) the name of the buildings dataframe column, np.array, or pd.Series where is stored building height. If set to None, height and ratio height/width will not be calculated. distance : int (default 10) distance between perpendicular ticks tick_length : int (default 50) lenght of ticks Attributes ---------- w : Series Series containing street profile width values wd : Series Series containing street profile standard deviation values o : Series Series containing street profile openness values h : Series Series containing street profile heights values. Returned only when heights is set. hd : Series Series containing street profile heights standard deviation values. Returned only when heights is set. p : Series Series containing street profile height/width ratio values. Returned only when heights is set. left : GeoDataFrame original left GeoDataFrame right : GeoDataFrame original right GeoDataFrame distance : int distance between perpendicular ticks tick_length : int lenght of ticks heights : GeoDataFrame Series containing used height values References ---------- Oliveira V (2013) Morpho: a methodology for assessing urban form. Urban Morphology 17(1): 21–33. Araldi A and Fusco G (2017) Decomposing and Recomposing Urban Fabric: The City from the Pedestrian Point of View. In: Gervasi O, Murgante B, Misra S, et al. (eds), Computational Science and Its Applications – ICCSA 2017, Lecture Notes in Computer Science, Cham: Springer International Publishing, pp. 365–376. Available from: http://link.springer.com/10.1007/978-3-319-62407-5. Examples -------- >>> street_profile = momepy.StreetProfile(streets_df, buildings_df, heights='height') 100%|██████████| 33/33 [00:02<00:00, 15.66it/s] >>> streets_df['width'] = street_profile.w >>> streets_df['deviations'] = street_profile.wd """
[docs] def __init__(self, left, right, heights=None, distance=10, tick_length=50): self.left = left self.right = right self.distance = distance self.tick_length = tick_length if heights is not None: if not isinstance(heights, str): right = right.copy() right["mm_h"] = heights heights = "mm_h" self.heights = right[heights] sindex = right.sindex results_list = [] deviations_list = [] heights_list = [] heights_deviations_list = [] openness_list = [] for idx, row in tqdm(left.iterrows(), total=left.shape[0]): # if idx == 2: # ee # list to hold all the point coords list_points = [] # set the current distance to place the point current_dist = distance # make shapely MultiLineString object shapely_line = row.geometry # get the total length of the line line_length = shapely_line.length # append the starting coordinate to the list list_points.append(Point(list(shapely_line.coords)[0])) # https://nathanw.net/2012/08/05/generating-chainage-distance-nodes-in-qgis/ # while the current cumulative distance is less than the total length of the line while current_dist < line_length: # use interpolate and increase the current distance list_points.append(shapely_line.interpolate(current_dist)) current_dist += distance # append end coordinate to the list list_points.append(Point(list(shapely_line.coords)[-1])) ticks = [] for num, pt in enumerate(list_points, 1): # start chainage 0 if num == 1: angle = self._getAngle(pt, list_points[num]) line_end_1 = self._getPoint1(pt, angle, tick_length / 2) angle = self._getAngle(line_end_1, pt) line_end_2 = self._getPoint2(line_end_1, angle, tick_length) tick1 = LineString([(line_end_1.x, line_end_1.y), (pt.x, pt.y)]) tick2 = LineString([(line_end_2.x, line_end_2.y), (pt.x, pt.y)]) ticks.append([tick1, tick2]) # everything in between if num < len(list_points) - 1: angle = self._getAngle(pt, list_points[num]) line_end_1 = self._getPoint1( list_points[num], angle, tick_length / 2 ) angle = self._getAngle(line_end_1, list_points[num]) line_end_2 = self._getPoint2(line_end_1, angle, tick_length) tick1 = LineString( [ (line_end_1.x, line_end_1.y), (list_points[num].x, list_points[num].y), ] ) tick2 = LineString( [ (line_end_2.x, line_end_2.y), (list_points[num].x, list_points[num].y), ] ) ticks.append([tick1, tick2]) # end chainage if num == len(list_points): angle = self._getAngle(list_points[num - 2], pt) line_end_1 = self._getPoint1(pt, angle, tick_length / 2) angle = self._getAngle(line_end_1, pt) line_end_2 = self._getPoint2(line_end_1, angle, tick_length) tick1 = LineString([(line_end_1.x, line_end_1.y), (pt.x, pt.y)]) tick2 = LineString([(line_end_2.x, line_end_2.y), (pt.x, pt.y)]) ticks.append([tick1, tick2]) # widths = [] m_heights = [] lefts = [] rights = [] for duo in ticks: for ix, tick in enumerate(duo): possible_intersections_index = list( sindex.intersection(tick.bounds) ) possible_intersections = right.iloc[possible_intersections_index] real_intersections = possible_intersections.intersects(tick) get_height = right.loc[list(real_intersections.index)] possible_int = get_height.exterior.intersection(tick) if not possible_int.is_empty.all(): true_int = [] for one in list(possible_int.index): if possible_int[one].type == "Point": true_int.append(possible_int[one]) elif possible_int[one].type == "MultiPoint": for p in possible_int[one]: true_int.append(p) if len(true_int) > 1: distances = [] ix = 0 for p in true_int: dist = p.distance(Point(tick.coords[-1])) distances.append(dist) ix = ix + 1 minimal = min(distances) if ix == 0: lefts.append(minimal) else: rights.append(minimal) else: if ix == 0: lefts.append( true_int[0].distance(Point(tick.coords[-1])) ) else: rights.append( true_int[0].distance(Point(tick.coords[-1])) ) if heights is not None: indices = {} for idx, row in get_height.iterrows(): dist = row.geometry.distance(Point(tick.coords[-1])) indices[idx] = dist minim = min(indices, key=indices.get) m_heights.append(right.loc[minim][heights]) openness = (len(lefts) + len(rights)) / len(ticks * 2) openness_list.append(1 - openness) if rights and lefts: results_list.append(2 * np.mean(lefts + rights)) deviations_list.append(np.std(lefts + rights)) elif not lefts and rights: results_list.append(2 * np.mean([np.mean(rights), tick_length / 2])) deviations_list.append(np.std(rights)) elif not rights and lefts: results_list.append(2 * np.mean([np.mean(lefts), tick_length / 2])) deviations_list.append(np.std(lefts)) else: results_list.append(tick_length) deviations_list.append(0) if heights is not None: if m_heights: heights_list.append(np.mean(m_heights)) heights_deviations_list.append(np.std(m_heights)) else: heights_list.append(0) heights_deviations_list.append(0) self.w = pd.Series(results_list, index=left.index) self.wd = pd.Series(deviations_list, index=left.index) self.o = pd.Series(openness_list, index=left.index) if heights is not None: self.h = pd.Series(heights_list, index=left.index) self.hd = pd.Series(heights_deviations_list, index=left.index) self.p = self.h / self.w
# http://wikicode.wikidot.com/get-angle-of-line-between-two-points # https://glenbambrick.com/tag/perpendicular/ # angle between two points def _getAngle(self, pt1, pt2): x_diff = pt2.x - pt1.x y_diff = pt2.y - pt1.y return math.degrees(math.atan2(y_diff, x_diff)) # start and end points of chainage tick # get the first end point of a tick def _getPoint1(self, pt, bearing, dist): angle = bearing + 90 bearing = math.radians(angle) x = pt.x + dist * math.cos(bearing) y = pt.y + dist * math.sin(bearing) return Point(x, y) # get the second end point of a tick def _getPoint2(self, pt, bearing, dist): bearing = math.radians(bearing) x = pt.x + dist * math.cos(bearing) y = pt.y + dist * math.sin(bearing) return Point(x, y)
[docs]class WeightedCharacter: """ Calculates the weighted character Character weighted by the area of the objects within `k` topological steps defined in spatial_weights. .. math:: \\frac{\\sum_{i=1}^{n} {character_{i} * area_{i}}}{\\sum_{i=1}^{n} area_{i}} Parameters ---------- gdf : GeoDataFrame GeoDataFrame containing objects to analyse values : str, list, np.array, pd.Series the name of the gdf dataframe column, np.array, or pd.Series where is stored character to be weighted spatial_weights : libpysal.weights spatial weights matrix - If None, Queen contiguity matrix of set order will be calculated based on left. unique_id : str name of the column with unique id used as spatial_weights index. areas : str, list, np.array, pd.Series (default None) the name of the left dataframe column, np.array, or pd.Series where is stored area value Attributes ---------- series : Series Series containing resulting values gdf : GeoDataFrame original GeoDataFrame values : GeoDataFrame Series containing used values areas : GeoDataFrame Series containing used areas sw : libpysal.weights spatial weights matrix id : Series Series containing used unique ID References ---------- Dibble J, Prelorendjos A, Romice O, et al. (2017) On the origin of spaces: Morphometric foundations of urban form evolution. Environment and Planning B: Urban Analytics and City Science 46(4): 707–730. Examples -------- >>> sw = libpysal.weights.DistanceBand.from_dataframe(tessellation_df, threshold=100, silence_warnings=True) >>> buildings_df['w_height_100'] = momepy.WeightedCharacter(buildings_df, values='height', spatial_weights=sw, unique_id='uID').series 100%|██████████| 144/144 [00:00<00:00, 361.60it/s] """
[docs] def __init__(self, gdf, values, spatial_weights, unique_id, areas=None): self.gdf = gdf self.sw = spatial_weights self.id = gdf[unique_id] data = gdf.copy() if areas is None: areas = gdf.geometry.area if not isinstance(areas, str): data["mm_a"] = areas areas = "mm_a" if not isinstance(values, str): data["mm_vals"] = values values = "mm_vals" self.areas = data[areas] self.values = data[values] data = data.set_index(unique_id) results_list = [] for index, row in tqdm(data.iterrows(), total=data.shape[0]): neighbours = spatial_weights.neighbors[index].copy() if neighbours: neighbours.append(index) else: neighbours = [index] subset = data.loc[neighbours] results_list.append( (sum(subset[values] * subset[areas])) / (sum(subset[areas])) ) self.series = pd.Series(results_list, index=gdf.index)
[docs]class CoveredArea: """ Calculates the area covered by neighbours Total area covered by neighbours defined in spatial_weights and element itself. .. math:: Parameters ---------- gdf : GeoDataFrame GeoDataFrame containing Polygon geometry spatial_weights : libpysal.weights spatial weights matrix unique_id : str name of the column with unique id used as spatial_weights index. Attributes ---------- series : Series Series containing resulting values gdf : GeoDataFrame original GeoDataFrame sw : libpysal.weights spatial weights matrix id : Series Series containing used unique ID Examples -------- >>> sw = momepy.sw_high(k=3, gdf=tessellation_df, ids='uID') >>> tessellation_df['covered3steps'] = mm.CoveredArea(tessellation_df, sw, 'uID').series 100%|██████████| 144/144 [00:00<00:00, 549.15it/s] """
[docs] def __init__(self, gdf, spatial_weights, unique_id): self.gdf = gdf self.sw = spatial_weights self.id = gdf[unique_id] data = gdf.set_index(unique_id) results_list = [] for index, row in tqdm(data.iterrows(), total=data.shape[0]): neighbours = spatial_weights.neighbors[index].copy() if neighbours: neighbours.append(index) else: neighbours = [index] areas = data.loc[neighbours].geometry.area results_list.append(sum(areas)) self.series = pd.Series(results_list, index=gdf.index)
[docs]class PerimeterWall: """ Calculate the perimeter wall length the joined structure. Parameters ---------- gdf : GeoDataFrame GeoDataFrame containing objects to analyse spatial_weights : libpysal.weights, optional spatial weights matrix - If None, Queen contiguity matrix will be calculated based on gdf. It is to denote adjacent buildings (note: based on index, not ID). Attributes ---------- series : Series Series containing resulting values gdf : GeoDataFrame original GeoDataFrame sw : libpysal.weights spatial weights matrix Examples -------- >>> buildings_df['wall_length'] = mm.PerimeterWall(buildings_df).series Calculating spatial weights... Spatial weights ready... 100%|██████████| 144/144 [00:00<00:00, 4171.39it/s] Notes ----- It might take a while to compute this character. """
[docs] def __init__(self, gdf, spatial_weights=None): self.gdf = gdf if spatial_weights is None: print("Calculating spatial weights...") from libpysal.weights import Queen spatial_weights = Queen.from_dataframe(gdf, silence_warnings=True) print("Spatial weights ready...") self.sw = spatial_weights # dict to store walls for each uID walls = {} components = pd.Series(spatial_weights.component_labels, index=range(len(gdf))) for i, (index, row) in tqdm(enumerate(gdf.iterrows()), total=gdf.shape[0]): # if the id is already present in walls, continue (avoid repetition) if i in walls: continue else: comp = spatial_weights.component_labels[i] to_join = components[components == comp].index joined = gdf.iloc[to_join] dissolved = joined.geometry.buffer( 0.01 ).unary_union # buffer to avoid multipolygons where buildings touch by corners only for b in to_join: walls[b] = dissolved.exterior.length results_list = [] for i, (index, row) in tqdm(enumerate(gdf.iterrows()), total=gdf.shape[0]): results_list.append(walls[i]) self.series = pd.Series(results_list, index=gdf.index)
[docs]class SegmentsLength: """ Calculate the cummulative or mean length of segments. Length of segments within set topological distance from each of them. Reached topological distance should be captured by spatial_weights. If mean=False it will return total length, if mean=True it will return mean value. Parameters ---------- gdf : GeoDataFrame GeoDataFrame containing streets (edges) to analyse spatial_weights : libpysal.weights, optional spatial weights matrix - If None, Queen contiguity matrix will be calculated based on streets (note: spatial_weights shoud be based on index, not unique ID). mean : boolean, optional If mean=False it will return total length, if mean=True it will return mean value Attributes ---------- series : Series Series containing resulting values gdf : GeoDataFrame original GeoDataFrame sw : libpysal.weights spatial weights matrix mean : boolean used mean boolean value Examples -------- >>> streets_df['length_neighbours'] = mm.SegmentsLength(streets_df, mean=True).series Calculating spatial weights... Spatial weights ready... """
[docs] def __init__(self, gdf, spatial_weights=None, mean=False): self.gdf = gdf self.mean = mean if spatial_weights is None: print("Calculating spatial weights...") from libpysal.weights import Queen spatial_weights = Queen.from_dataframe(gdf, silence_warnings=True) print("Spatial weights ready...") self.sw = spatial_weights results_list = [] for index, row in tqdm(gdf.iterrows(), total=gdf.shape[0]): neighbours = spatial_weights.neighbors[index].copy() if neighbours: neighbours.append(index) else: neighbours = [index] dims = gdf.iloc[neighbours].geometry.length if mean: results_list.append(np.mean(dims)) else: results_list.append(sum(dims)) self.series = pd.Series(results_list, index=gdf.index)