def __init__(self, gdf, block_id, spatial_weights=None): self.gdf = gdf results_list = [] gdf = gdf.copy() if not isinstance(block_id, str): gdf["mm_bid"] = block_id block_id = "mm_bid" self.block_id = gdf[block_id] # if weights matrix is not passed, generate it from objects if spatial_weights is None: print("Calculating spatial weights...") from libpysal.weights import Queen spatial_weights = Queen.from_dataframe(gdf, silence_warnings=True) self.sw = spatial_weights # dict to store nr of courtyards for each uID courtyards = {} components = pd.Series(spatial_weights.component_labels, index=gdf.index) for index in tqdm(gdf.index, total=gdf.shape[0]): # if the id is already present in courtyards, continue (avoid repetition) if index in courtyards: continue else: comp = spatial_weights.component_labels[index] to_join = components[components == comp].index joined = gdf.loc[to_join] dissolved = joined.geometry.buffer( 0.01 ).unary_union # buffer to avoid multipolygons where buildings touch by corners only try: interiors = len(list(dissolved.interiors)) except (ValueError): print("Something unexpected happened.") for b in to_join: courtyards[b] = interiors # fill dict with values # copy values from dict to gdf for index, row in tqdm(gdf.iterrows(), total=gdf.shape[0]): results_list.append(courtyards[index]) self.series = pd.Series(results_list, index=gdf.index)
def __init__(self, gdf, spatial_weights_higher, unique_id, spatial_weights=None, verbose=True): self.gdf = gdf self.sw_higher = spatial_weights_higher self.id = gdf[unique_id] results_list = [] # if weights matrix is not passed, generate it from gdf if spatial_weights is None: print("Calculating spatial weights...") if verbose else None from libpysal.weights import Queen spatial_weights = Queen.from_dataframe(gdf, silence_warnings=True, ids=unique_id) print("Spatial weights ready...") if verbose else None self.sw = spatial_weights patches = dict(zip(gdf[unique_id], spatial_weights.component_labels)) for uid in tqdm( self.id, total=gdf.shape[0], disable=not verbose, desc="Calculating adjacency", ): if uid in spatial_weights_higher.neighbors.keys(): neighbours = spatial_weights_higher.neighbors[uid].copy() if neighbours: neighbours.append(uid) patches_sub = [patches[x] for x in neighbours] patches_nr = len(set(patches_sub)) results_list.append(patches_nr / len(neighbours)) else: results_list.append(np.nan) else: results_list.append(np.nan) self.series = pd.Series(results_list, index=gdf.index)
def __init__(self, gdf, block_id=None, spatial_weights=None, verbose=True): if block_id is not None: warnings.warn( "block_id is deprecated and will be removed in v0.4.", FutureWarning, ) self.gdf = gdf results_list = [] gdf = gdf.copy() # if weights matrix is not passed, generate it from objects if spatial_weights is None: print("Calculating spatial weights...") if verbose else None from libpysal.weights import Queen spatial_weights = Queen.from_dataframe(gdf, silence_warnings=True) self.sw = spatial_weights # dict to store nr of courtyards for each uID courtyards = {} components = pd.Series(spatial_weights.component_labels, index=gdf.index) for i, index in tqdm( enumerate(gdf.index), total=gdf.shape[0], disable=not verbose ): # if the id is already present in courtyards, continue (avoid repetition) if index in courtyards: continue else: comp = spatial_weights.component_labels[i] to_join = components[components == comp].index joined = gdf.loc[to_join] dissolved = joined.geometry.buffer( 0.01 ).unary_union # buffer to avoid multipolygons where buildings touch by corners only try: interiors = len(list(dissolved.interiors)) except (ValueError): print("Something unexpected happened.") for b in to_join: courtyards[b] = interiors # fill dict with values results_list = [courtyards[index] for index in gdf.index] self.series = pd.Series(results_list, index=gdf.index)
def test_BuildingAdjacencyy(self): sw = Queen.from_dataframe(self.df_buildings, ids="uID") swh = mm.sw_high(k=3, gdf=self.df_tessellation, ids="uID") self.df_buildings["adj_sw"] = mm.BuildingAdjacency( self.df_buildings, spatial_weights=sw, unique_id="uID", spatial_weights_higher=swh, ).series self.df_buildings["adj_sw_none"] = mm.BuildingAdjacency( self.df_buildings, unique_id="uID", spatial_weights_higher=swh).series check = 0.2613824113909074 assert self.df_buildings["adj_sw"].mean() == check assert self.df_buildings["adj_sw_none"].mean() == check swh_drop = mm.sw_high(k=3, gdf=self.df_tessellation[2:], ids="uID") assert (mm.BuildingAdjacency( self.df_buildings, unique_id="uID", spatial_weights_higher=swh_drop).series.isna().any())
def __init__(self, gdf, spatial_weights=None, verbose=True): self.gdf = gdf if spatial_weights is None: print("Calculating spatial weights...") if verbose else None from libpysal.weights import Queen spatial_weights = Queen.from_dataframe(gdf, silence_warnings=True) print("Spatial weights ready...") if verbose else None self.sw = spatial_weights # dict to store walls for each uID walls = {} components = pd.Series(spatial_weights.component_labels, index=range(len(gdf))) geom = gdf.geometry for i in tqdm(range(gdf.shape[0]), total=gdf.shape[0], disable=not verbose): # 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 = geom.iloc[to_join] dissolved = joined.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 in tqdm(range(gdf.shape[0]), total=gdf.shape[0], disable=not verbose): results_list.append(walls[i]) self.series = pd.Series(results_list, index=gdf.index)
def __init__(self, gdf, spatial_weights_higher, unique_id, spatial_weights=None): self.gdf = gdf self.sw_higher = spatial_weights_higher self.id = gdf[unique_id] results_list = [] # if weights matrix is not passed, generate it from gdf if spatial_weights is None: print("Calculating spatial weights...") from libpysal.weights import Queen spatial_weights = Queen.from_dataframe(gdf, silence_warnings=True, ids=unique_id) print("Spatial weights ready...") self.sw = spatial_weights patches = dict(zip(gdf[unique_id], spatial_weights.component_labels)) print("Calculating adjacency...") for index, row in tqdm(gdf.iterrows(), total=gdf.shape[0]): neighbours = spatial_weights_higher.neighbors[ row[unique_id]].copy() if neighbours: neighbours.append(row[unique_id]) patches_sub = [patches[x] for x in neighbours] patches_nr = len(set(patches_sub)) results_list.append(patches_nr / len(neighbours)) else: results_list.append(0) self.series = pd.Series(results_list, index=gdf.index)
import ols as OLS from utils import optim_moments, RegressionPropsY, get_spFilter, spdot # import user_output as USER # import summary_output as SUMMARY # import regimes as REGI # First import libpysal to load the spatial analysis tools. import libpysal from libpysal.examples import load_example from libpysal.weights import Queen # Open data on NCOVR US County Homicides (3085 areas). nat = load_example('Natregimes') db = libpysal.io.open(nat.get_path('natregimes.dbf'),'r') nat_shp = libpysal.examples.get_path("natregimes.shp") w = Queen.from_shapefile(nat_shp) w.transform = 'r' name_y = ['HR70','HR80','HR90'] y = np.array([db.by_col(name) for name in name_y]).T name_x = ['RD70','RD80','RD90','PS70','PS80','PS90'] x = np.array([db.by_col(name) for name in name_x]).T full_weights = False def _moments_kkp(ws, u, i, trace_w2=None): ''' Compute G and g matrices for the KKP model. ...
def _spatial_dissim(data, group_pop_var, total_pop_var, w=None, standardize=False): """Calculate of Spatial Dissimilarity index. Parameters ---------- data : a geopandas DataFrame with a geometry column. group_pop_var : string The name of variable in data that contains the population size of the group of interest total_pop_var : string The name of variable in data that contains the total population of the unit w : W A PySAL weights object. If not provided, Queen contiguity matrix is used. standardize : boolean A condition for row standardisation of the weights matrices. If True, the values of cij in the formulas gets row standardized. For the sake of comparison, the seg R package of Hong, Seong-Yun, David O'Sullivan, and Yukio Sadahiro. "Implementing spatial segregation measures in R." PloS one 9.11 (2014): e113767. works by default with row standardization. Returns ---------- statistic : float Spatial Dissimilarity Index core_data : a geopandas DataFrame A geopandas DataFrame that contains the columns used to perform the estimate. Notes ----- Based on Morrill, R. L. (1991) "On the Measure of Geographic Segregation". Geography Research Forum. Reference: :cite:`morrill1991measure`. """ if type(standardize) is not bool: raise TypeError("std is not a boolean object") if w is None: w_object = Queen.from_dataframe(data) else: w_object = w if not issubclass(type(w_object), libpysal.weights.W): raise TypeError("w is not a PySAL weights object") D = _dissim(data, group_pop_var, total_pop_var)[0] x = np.array(data[group_pop_var]) t = np.array(data[total_pop_var]) # If a unit has zero population, the group of interest frequency is zero pi = np.where(t == 0, 0, x / t) if not standardize: cij = w_object.sparse.toarray() else: cij = w_object.sparse.toarray() cij = cij / cij.sum(axis=1).reshape((cij.shape[0], 1)) # Inspired in (second solution): https://stackoverflow.com/questions/22720864/efficiently-calculating-a-euclidean-distance-matrix-using-numpy # Distance Matrix abs_dist = abs(pi[..., np.newaxis] - pi) # manhattan_distances used to compute absolute distances num = np.multiply(abs_dist, cij).sum() den = cij.sum() SD = D - num / den SD core_data = data[[group_pop_var, total_pop_var, data.geometry.name]] return SD, core_data
def mean_interbuilding_distance(objects, tessellation, unique_id, spatial_weights=None, spatial_weights_higher=None, order=3): """ Calculate the mean interbuilding distance within x topological steps Interbuilding distances are calculated between buildings on adjacent cells based on `spatial_weights`. .. math:: Parameters ---------- objects : GeoDataFrame GeoDataFrame containing objects to analyse tessellation : GeoDataFrame GeoDataFrame containing morphological tessellation - source of spatial_weights and spatial_weights_higher. It is crucial to use exactly same input as was used durign the calculation of weights matrix and spatial_weights_higher. If spatial_weights or spatial_weights_higher is None, tessellation is used to calulate it. unique_id : str name of the column with unique id spatial_weights : libpysal.weights, optional spatial weights matrix - If None, Queen contiguity matrix will be calculated based on tessellation spatial_weights_higher : libpysal.weights, optional spatial weights matrix - If None, Queen contiguity of higher order will be calculated based on tessellation order : int Order of Queen contiguity Returns ------- Series Series containing resulting values. References --------- ADD, but it is adapted quite a lot. Notes ----- Fix terminology, it is unclear. Fix UserWarning. Examples -------- >>> buildings_df['mean_interbuilding_distance'] = momepy.mean_interbuilding_distance(buildings_df, tessellation_df, 'uID') Calculating mean interbuilding distances... Generating weights matrix (Queen)... Generating weights matrix (Queen) of 3 topological steps... Generating adjacency matrix based on weights matrix... Computing interbuilding distances... 100%|██████████| 746/746 [00:03<00:00, 200.14it/s] Computing mean interbuilding distances... 100%|██████████| 144/144 [00:00<00:00, 317.42it/s] Mean interbuilding distances calculated. >>> buildings_df['mean_interbuilding_distance'][0] 29.305457092042744 """ if not all(tessellation.index == range(len(tessellation))): raise ValueError( 'Index is not consecutive range 0:x, spatial weights will not match objects.' ) print('Calculating mean interbuilding distances...') if spatial_weights is None: print('Generating weights matrix (Queen)...') from libpysal.weights import Queen # matrix to capture interbuilding relationship spatial_weights = Queen.from_dataframe(tessellation) if spatial_weights_higher is None: print('Generating weights matrix (Queen) of {} topological steps...'. format(order)) from momepy import Queen_higher # matrix to define area of analysis (more steps) spatial_weights_higher = Queen_higher(k=order, geodataframe=tessellation) # define empty list for results results_list = [] print('Generating adjacency matrix based on weights matrix...') # define adjacency list from lipysal adj_list = spatial_weights.to_adjlist() adj_list['distance'] = -1 print('Computing interbuilding distances...') # measure each interbuilding distance of neighbours and save them to adjacency list for index, row in tqdm(adj_list.iterrows(), total=adj_list.shape[0]): inverted = adj_list[(adj_list.focal == row.neighbor)][( adj_list.neighbor == row.focal)].iloc[0]['distance'] if inverted == -1: object_id = tessellation.iloc[row.focal.astype(int)][unique_id] building_object = objects.loc[objects[unique_id] == object_id] neighbours_id = tessellation.iloc[row.neighbor.astype( int)][unique_id] building_neighbour = objects.loc[objects[unique_id] == neighbours_id] adj_list.loc[ index, 'distance'] = building_neighbour.iloc[0].geometry.distance( building_object.iloc[0].geometry) else: adj_list.at[index, 'distance'] = inverted print('Computing mean interbuilding distances...') # iterate over objects to get the final values for index, row in tqdm(objects.iterrows(), total=objects.shape[0]): # id to match spatial weights uid = tessellation.loc[tessellation[unique_id] == row[unique_id]].index[0] # define neighbours based on weights matrix defining analysis area neighbours = spatial_weights_higher.neighbors[uid] neighbours.append(uid) if neighbours: selection = adj_list[adj_list.focal.isin(neighbours)][ adj_list.neighbor.isin(neighbours)] results_list.append(np.nanmean(selection.distance)) series = pd.Series(results_list) print('Mean interbuilding distances calculated.') return series
def building_adjacency(objects, tessellation, spatial_weights=None, spatial_weights_higher=None, order=3, unique_id='uID'): """ Calculate the level of building adjacency Building adjacency reflects how much buildings tend to join together into larger structures. It is calculated as a ratio of joined built-up structures and buildings within k topological steps. .. math:: Parameters ---------- objects : GeoDataFrame GeoDataFrame containing objects to analyse tessellation : GeoDataFrame GeoDataFrame containing morphological tessellation - source of spatial_weights and spatial_weights_higher. It is crucial to use exactly same input as was used durign the calculation of weights matrix and spatial_weights_higher. If spatial_weights or spatial_weights_higher is None, tessellation is used to calulate it. spatial_weights : libpysal.weights, optional spatial weights matrix - If None, Queen contiguity matrix will be calculated based on tessellation spatial_weights_higher : libpysal.weights, optional spatial weights matrix - If None, Queen contiguity of higher order will be calculated based on tessellation order : int Order of Queen contiguity Returns ------- Series Series containing resulting values. References --------- Vanderhaegen S and Canters F (2017) Mapping urban form and function at city block level using spatial metrics. Landscape and Urban Planning 167: 399–409. Examples -------- >>> buildings_df['adjacency'] = momepy.building_adjacency(buildings_df, tessellation_df, unique_id='uID') Calculating adjacency... Calculating spatial weights... Spatial weights ready... Generating weights matrix (Queen) of 3 topological steps... Generating dictionary of built-up patches... 100%|██████████| 144/144 [00:00<00:00, 9301.73it/s] Calculating adjacency within k steps... 100%|██████████| 144/144 [00:00<00:00, 335.55it/s] Adjacency calculated. >>> buildings_df['adjacency'][10] 0.23809523809523808 """ # define empty list for results results_list = [] print('Calculating adjacency...') if not all(tessellation.index == range(len(tessellation))): raise ValueError( 'Index is not consecutive range 0:x, spatial weights will not match objects.' ) # if weights matrix is not passed, generate it from objects if spatial_weights is None: print('Calculating spatial weights...') from libpysal.weights import Queen spatial_weights = Queen.from_dataframe(objects, silence_warnings=True) print('Spatial weights ready...') if spatial_weights_higher is None: print('Generating weights matrix (Queen) of {} topological steps...'. format(order)) from momepy import Queen_higher # matrix to define area of analysis (more steps) spatial_weights_higher = Queen_higher(k=order, geodataframe=tessellation) print('Generating dictionary of built-up patches...') # dict to store nr of courtyards for each uID patches = {} jID = 1 for index, row in tqdm(objects.iterrows(), total=objects.shape[0]): # if the id is already present in courtyards, continue (avoid repetition) if index in patches: continue else: to_join = [index ] # list of indices which should be joined together neighbours = [] # list of neighbours weights = spatial_weights.neighbors[ index] # neighbours from spatial weights for w in weights: neighbours.append(w) # make a list from weigths for n in neighbours: while n not in to_join: # until there is some neighbour which is not in to_join to_join.append(n) weights = spatial_weights.neighbors[n] for w in weights: neighbours.append( w ) # extend neighbours by neighbours of neighbours :) for b in to_join: patches[b] = jID # fill dict with values jID = jID + 1 print('Calculating adjacency within k steps...') for index, row in tqdm(objects.iterrows(), total=objects.shape[0]): uid = tessellation.loc[tessellation[unique_id] == row[unique_id]].index[0] neighbours = spatial_weights_higher.neighbors[uid] neighbours_ids = tessellation.iloc[neighbours][unique_id] neighbours_ids = neighbours_ids.append( pd.Series(row[unique_id], index=[index])) building_neighbours = objects.loc[objects[unique_id].isin( neighbours_ids)] indices = list(building_neighbours.index) patches_sub = [patches[x] for x in indices] patches_nr = len(set(patches_sub)) results_list.append(patches_nr / len(building_neighbours)) series = pd.Series(results_list) print('Adjacency calculated.') return series
def run_stats(self): """Main function which do the process.""" # Get the common fields..currentField() self.admin_layer = self.cbx_aggregation_layer.currentLayer() input_name = self.admin_layer.name() field = self.cbx_indicator_field.currentField() self.layer = QgsProject.instance().mapLayersByName(input_name)[0] # Output. self.output_file_path = self.le_output_filepath.text() try: self.button_box_ok.setDisabled(True) # noinspection PyArgumentList QApplication.setOverrideCursor(Qt.WaitCursor) # noinspection PyArgumentList QApplication.processEvents() if not self.admin_layer: raise NoLayerProvidedException if not self.admin_layer and self.use_point_layer: raise NoLayerProvidedException crs_admin_layer = self.admin_layer.crs() # Output if not self.output_file_path: temp_file = NamedTemporaryFile(delete=False, suffix='-geopublichealth.shp') self.output_file_path = temp_file.name temp_file.flush() temp_file.close() else: with open(self.output_file_path, 'w') as document: pass admin_layer_provider = self.layer.dataProvider() fields = admin_layer_provider.fields() if admin_layer_provider.fields().indexFromName( self.name_field) != -1: raise FieldExistingException(field=self.name_field) fields.append(QgsField('LISA_P', QVariant.Double)) fields.append(QgsField('LISA_Z', QVariant.Double)) fields.append(QgsField('LISA_Q', QVariant.Int)) fields.append(QgsField('LISA_I', QVariant.Double)) fields.append(QgsField('LISA_C', QVariant.Double)) # The QgsVectorFileWriter was Deprecated since 3.10 However,....... #The create() function DOEST NOT Flush the feature unless QGIS close. #options = QgsVectorFileWriter.SaveVectorOptions() #options.driverName = "ESRI Shapefile" #file_writer=QgsVectorFileWriter.create(self.output_file_path,fields,QgsWkbTypes.Polygon,self.admin_layer.crs(),QgsCoordinateTransformContext(),options) #It's currently a bug https://github.com/qgis/QGIS/issues/35021 # So I will keep it for now file_writer = QgsVectorFileWriter(self.output_file_path, 'utf-8', fields, QgsWkbTypes.Polygon, self.admin_layer.crs(), 'ESRI Shapefile') if self.cbx_contiguity.currentIndex() == 0: # queen # fix_print_with_import print('Info: Local Moran\'s using queen contiguity') #Pysal 2.0 change #https://github.com/pysal/pysal/blob/master/MIGRATING.md w = Queen.from_shapefile(self.admin_layer.source()) else: # 1 for rook # fix_print_with_import print('Info: Local Moran\'s using rook contiguity') w = Rook.from_shapefile(self.admin_layer.source()) #Pysal 2.0 #https://stackoverflow.com/questions/59455383/pysal-does-not-have-attribute-open import geopandas f = geopandas.read_file(self.admin_layer.source().replace( '.shp', '.dbf')) y = f[str(field)] lm = Moran_Local(y, w, transformation="r", permutations=999) sig_q = lm.q * (lm.p_sim <= 0.05 ) # could make significance level an option outFeat = QgsFeature() i = 0 count = self.admin_layer.featureCount() for i, feature in enumerate(self.admin_layer.getFeatures()): attributes = feature.attributes() attributes.append(float(lm.p_sim[i])) attributes.append(float(lm.z_sim[i])) attributes.append(int(lm.q[i])) attributes.append(float(lm.Is[i])) attributes.append(int(sig_q[i])) new_feature = QgsFeature() new_geom = QgsGeometry(feature.geometry()) new_feature.setAttributes(attributes) new_feature.setGeometry(new_geom) file_writer.addFeature(new_feature) del file_writer self.output_layer = QgsVectorLayer(self.output_file_path, "LISA Moran's I - " + field, 'ogr') QgsProject.instance().addMapLayer(self.output_layer) self.add_symbology() self.signalStatus.emit(3, tr('Successful process')) except GeoPublicHealthException as e: display_message_bar(msg=e.msg, level=e.level, duration=e.duration) finally: self.button_box_ok.setDisabled(False) # noinspection PyArgumentList QApplication.restoreOverrideCursor() # noinspection PyArgumentList QApplication.processEvents()
def neighbour_distance(objects, tessellation, unique_id, spatial_weights=None): """ Calculate the mean distance to buildings on adjacent cells .. math:: \\frac{1}{n}\\sum_{i=1}^n dist_i=\\frac{dist_1+dist_2+\\cdots+dist_n}{n} Parameters ---------- objects : GeoDataFrame GeoDataFrame containing objects to analyse tessellation : GeoDataFrame GeoDataFrame containing morphological tessellation - source of spatial_weights. It is crucial to use exactly same input as was used durign the calculation of weights matrix. If spatial_weights is None, tessellation is used to calulate it. unique_id : str name of the column with unique id spatial_weights : libpysal.weights, optional spatial weights matrix - If None, Queen contiguity matrix will be calculated based on tessellation Returns ------- Series Series containing resulting values. References --------- Schirmer PM and Axhausen KW (2015) A multiscale classification of urban morphology. Journal of Transport and Land Use 9(1): 101–130. Examples -------- >>> buildings_df['neighbour_distance'] = momepy.neighbour_distance(buildings_df, tessellation_df, 'uID') Calculating distances... Calculating spatial weights... Spatial weights ready... 100%|██████████| 144/144 [00:00<00:00, 345.78it/s] Distances calculated. >>> buildings_df['neighbour_distance'][0] 29.18589019096464 """ # define empty list for results results_list = [] print('Calculating distances...') if not all(tessellation.index == range(len(tessellation))): raise ValueError( 'Index is not consecutive range 0:x, spatial weights will not match objects.' ) if spatial_weights is None: print('Calculating spatial weights...') from libpysal.weights import Queen spatial_weights = Queen.from_dataframe(tessellation) print('Spatial weights ready...') # iterating over rows one by one for index, row in tqdm(objects.iterrows(), total=objects.shape[0]): uid = tessellation.loc[tessellation[unique_id] == row[unique_id]].index[0] neighbours = spatial_weights.neighbors[uid] neighbours_ids = tessellation.iloc[neighbours][unique_id] building_neighbours = objects.loc[objects[unique_id].isin( neighbours_ids)] if len(building_neighbours) > 0: results_list.append( np.mean(building_neighbours.geometry.distance( row['geometry']))) else: results_list.append(0) series = pd.Series(results_list) print('Distances calculated.') return series
def greedy( gdf, strategy="balanced", balance="count", min_colors=4, sw="queen", min_distance=None, silence_warnings=True, interchange=False, ): """ Color GeoDataFrame using various strategies of greedy (topological) colouring. Attempts to color a GeoDataFrame using as few colors as possible, where no neighbours can have same color as the feature itself. Offers various strategies ported from QGIS or implemented within networkX for greedy graph coloring. ``greedy`` will return pandas.Series representing assinged color codes. Parameters ---------- gdf : GeoDataFrame GeoDataFrame strategy : str (default 'balanced') Determine coloring strategy. Options are ``'balanced'`` for algorithm based on QGIS Topological coloring. It is aiming for a visual balance, defined by the balance parameter. Other options are those supported by networkx.greedy_color: * ``'largest_first'`` * ``'random_sequential'`` * ``'smallest_last'`` * ``'independent_set'`` * ``'connected_sequential_bfs'`` * ``'connected_sequential_dfs'`` * ``'connected_sequential'`` (alias for the previous strategy) * ``'saturation_largest_first'`` * ``'DSATUR'`` (alias for the previous strategy) For details see https://networkx.github.io/documentation/stable/reference/algorithms/generated/networkx.algorithms.coloring.greedy_color.html balance : str (default 'count') If strategy is ``'balanced'``, determine the method of color balancing. * ``'count'`` attempts to balance the number of features per each color. * ``'area'`` attempts to balance the area covered by each color. * ``'centroid'`` attempts to balance the distance between colors based on the distance between centroids. * ``'distance'`` attempts to balance the distance between colors based on the distance between geometries. Slower than ``'centroid'``, but more precise. ``'centroid'`` and ``'distance'`` are significantly slower than other especially for larger GeoDataFrames. Apart from ``'count'``, all require CRS to be projected (not in degrees) to ensure metric values are correct. min_colors: int (default 4) If strategy is ``'balanced'``, define the minimal number of colors to be used. sw : 'queen', 'rook' or libpysal.weights.W (default 'queen') If min_distance is None, one can pass ``'libpysal.weights.W'`` object denoting neighbors or let greedy to generate one based on ``'queen'`` or ``'rook'`` contiguity. min_distance : float Set minimal distance between colors. If min_distance is not None, slower algorithm for generating spatial weghts is used based on intersection between geometries. Min_distance is then used as a tolerance of intersection. silence_warnings : bool (default True) Silence libpysal warnings when creating spatial weights. interchange : bool (defaul False) Use the color interchange algorithm (applicable for networkx strategies) For details see https://networkx.github.io/documentation/stable/reference/algorithms/generated/networkx.algorithms.coloring.greedy_color.html Examples -------- Default: >>> gdf['greedy_colors'] = greedy(gdf) Balanced by area: >>> gdf['balanced_area'] = greedy(gdf, strategy='balanced', >>> balance='area') Using rook adjacency: >>> gdf['rook_adjacency'] = greedy(gdf, sw='rook') Adding minimal distance between colors: >>> gdf['min_distance'] = greedy(gdf, min_distance=100) Using different coloring strategy: >>> gdf['smallest_last'] = greedy(gdf, strategy='smallest_last') Returns ------- color : pd.Series pandas.Series representing assinged color codes """ if min_distance is not None: sw = _geos_sw(gdf, tolerance=min_distance, silence_warnings=silence_warnings) if not isinstance(sw, W): if sw == "queen": sw = Queen.from_dataframe(gdf, ids=gdf.index.to_list(), silence_warnings=silence_warnings) elif sw == "rook": sw = Rook.from_dataframe(gdf, ids=gdf.index.to_list(), silence_warnings=silence_warnings) if strategy == "balanced": return pd.Series( _balanced(gdf, sw, balance=balance, min_colors=min_colors)) elif strategy in STRATEGIES: color = nx.greedy_color(sw.to_networkx(), strategy=strategy, interchange=interchange) color = pd.Series(color).sort_index() color.index = gdf.index return color else: raise ValueError("{} is not a valid strategy.".format(strategy))
def neighbours(objects, spatial_weights=None, weighted=False): """ Calculate the number of topological neighbours of each object. Topological neighbours are defined by queen adjacency. If weighted=True, number of neighbours will be divided by the perimeter of object, to return relative value. .. math:: Parameters ---------- objects : GeoDataFrame GeoDataFrame containing objects to analyse spatial_weights : libpysal.weights (default None) spatial weights matrix - If None, Queen contiguity matrix will be calculated based on tessellation weighted : bool (default False) if weighted=True, number of neighbours will be divided by the perimeter of object, to return relative value Returns ------- Series Series containing resulting values. References --------- Hermosilla T, Ruiz LA, Recio JA, et al. (2012) Assessing contextual descriptive features for plot-based classification of urban areas. Landscape and Urban Planning, Elsevier B.V. 106(1): 124–137. Examples -------- >>> tessellation_df['neighbours'] = momepy.neighbours(tessellation_df) Calculating spatial weights... Spatial weights ready... Calculating neighbours... 100%|██████████| 144/144 [00:00<00:00, 6909.50it/s] Neighbours calculated. >>> tessellation_df['neighbours'][0] 4 """ if not all(objects.index == range(len(objects))): raise ValueError( 'Index is not consecutive range 0:x, spatial weights will not match objects.' ) # if weights matrix is not passed, generate it from objects if spatial_weights is None: print('Calculating spatial weights...') from libpysal.weights import Queen spatial_weights = Queen.from_dataframe(objects, silence_warnings=True) print('Spatial weights ready...') print('Calculating neighbours...') neighbours = [] for index, row in tqdm(objects.iterrows(), total=objects.shape[0]): if weighted is True: neighbours.append(spatial_weights.cardinalities[index] / row.geometry.length) else: neighbours.append(spatial_weights.cardinalities[index]) series = pd.Series(neighbours) print('Neighbours calculated.') return series
def _spatial_prox_profile(data, group_pop_var, total_pop_var, m=1000): """ Calculation of Spatial Proximity Profile Parameters ---------- data : a geopandas DataFrame with a geometry column. group_pop_var : string The name of variable in data that contains the population size of the group of interest total_pop_var : string The name of variable in data that contains the total population of the unit m : int a numeric value indicating the number of thresholds to be used. Default value is 1000. A large value of m creates a smoother-looking graph and a more precise spatial proximity profile value but slows down the calculation speed. Attributes ---------- statistic : float Spatial Proximity Index core_data : a geopandas DataFrame A geopandas DataFrame that contains the columns used to perform the estimate. Notes ----- Based on Hong, Seong-Yun, and Yukio Sadahiro. "Measuring geographic segregation: a graph-based approach." Journal of Geographical Systems 16.2 (2014): 211-231. """ if (str(type(data)) != '<class \'geopandas.geodataframe.GeoDataFrame\'>'): raise TypeError( 'data is not a GeoDataFrame and, therefore, this index cannot be calculated.' ) if ('geometry' not in data.columns): data['geometry'] = data[data._geometry_column_name] data = data.drop([data._geometry_column_name], axis=1) data = data.set_geometry('geometry') if (type(m) is not int): raise TypeError('m must be a string.') if (m < 2): raise ValueError('m must be greater than 1.') if ((type(group_pop_var) is not str) or (type(total_pop_var) is not str)): raise TypeError('group_pop_var and total_pop_var must be strings') if ((group_pop_var not in data.columns) or (total_pop_var not in data.columns)): raise ValueError( 'group_pop_var and total_pop_var must be variables of data') data = data.rename(columns={ group_pop_var: 'group_pop_var', total_pop_var: 'total_pop_var' }) if any(data.total_pop_var < data.group_pop_var): raise ValueError( 'Group of interest population must equal or lower than the total population of the units.' ) wij = Queen.from_dataframe(data).full()[0] delta = manhattan_distances(wij) def calculate_etat(t): g_t_i = np.where(data.group_pop_var / data.total_pop_var >= t, True, False) k = g_t_i.sum() sub_delta_ij = delta[g_t_i, :][:, g_t_i] den = sub_delta_ij.sum() eta_t = (k**2 - k) / den return eta_t grid = np.linspace(0, 1, m) aux = np.array(list(map(calculate_etat, grid))) aux[aux == inf] = 0 aux[aux == -inf] = 0 curve = np.nan_to_num(aux, 0) threshold = data.group_pop_var.sum() / data.total_pop_var.sum() SPP = ((threshold - ((curve[grid < threshold]).sum() / m - (curve[grid >= threshold]).sum() / m)) / (1 - threshold)) core_data = data[['group_pop_var', 'total_pop_var', 'geometry']] return SPP, grid, curve, core_data
def _test(): import doctest start_suppress = np.get_printoptions()['suppress'] np.set_printoptions(suppress=True) doctest.testmod() np.set_printoptions(suppress=start_suppress) if __name__ == '__main__': _test() import numpy as np import libpysal from .sur_utils import sur_dictxy, sur_dictZ from libpysal.examples import load_example from libpysal.weights import Queen nat = load_example('Natregimes') db = libpysal.io.open(nat.get_path('natregimes.dbf'), 'r') y_var = ['HR80', 'HR90'] x_var = [['PS80', 'UE80'], ['PS90', 'UE90']] w = Queen.from_shapefile(nat.get_path("natregimes.shp")) w.transform = 'r' bigy0, bigX0, bigyvars0, bigXvars0 = sur_dictxy(db, y_var, x_var) reg0 = SURerrorML(bigy0,bigX0,w,regimes=regimes,name_bigy=bigyvars0,name_bigX=bigXvars0,\ name_w="natqueen",name_ds="natregimes",vm=True,nonspat_diag=True,spat_diag=True) # reg0 = SURerrorGM(bigy0,bigX0,w,regimes=regimes,name_bigy=bigyvars0,name_bigX=bigXvars0,\ # name_w="natqueen",name_ds="natregimes",vm=False,nonspat_diag=True,spat_diag=False) print(reg0.summary)
def courtyards(objects, block_id, spatial_weights=None): """ Calculate the number of courtyards within the joined structure. Parameters ---------- objects : GeoDataFrame GeoDataFrame containing objects to analyse block_id : str name of the column where is stored block ID spatial_weights : libpysal.weights, optional spatial weights matrix - If None, Queen contiguity matrix will be calculated based on objects. It is to denote adjacent buildings. Returns ------- Series Series containing resulting values. Notes ----- Script is not optimised at all, so it is currently extremely slow. """ # define empty list for results results_list = [] print('Calculating courtyards...') if not all(objects.index == range(len(objects))): raise ValueError( 'Index is not consecutive range 0:x, spatial weights will not match objects.' ) # if weights matrix is not passed, generate it from objects if spatial_weights is None: print('Calculating spatial weights...') from libpysal.weights import Queen spatial_weights = Queen.from_dataframe(objects, silence_warnings=True) print('Spatial weights ready...') # dict to store nr of courtyards for each uID courtyards = {} for index, row in tqdm(objects.iterrows(), total=objects.shape[0]): # if the id is already present in courtyards, continue (avoid repetition) if index in courtyards: continue else: to_join = [index ] # list of indices which should be joined together neighbours = [] # list of neighbours weights = spatial_weights.neighbors[ index] # neighbours from spatial weights for w in weights: neighbours.append(w) # make a list from weigths for n in neighbours: while n not in to_join: # until there is some neighbour which is not in to_join to_join.append(n) weights = spatial_weights.neighbors[n] for w in weights: neighbours.append( w ) # extend neighbours by neighbours of neighbours :) joined = objects.iloc[to_join] dissolved = joined.geometry.buffer( 0.01 ).unary_union # buffer to avoid multipolygons where buildings touch by corners only try: interiors = len(list(dissolved.interiors)) except (ValueError): print('Something happened.') for b in to_join: courtyards[b] = interiors # fill dict with values # copy values from dict to gdf for index, row in tqdm(objects.iterrows(), total=objects.shape[0]): results_list.append(courtyards[index]) series = pd.Series(results_list) print('Courtyards calculated.') return series
def blocks(cells, streets, buildings, id_name, unique_id): """ Generate blocks based on buildings, tesselation and street network Adds bID to buildings and tesselation. Parameters ---------- cells : GeoDataFrame GeoDataFrame containing morphological tessellation streets : GeoDataFrame GeoDataFrame containing street network buildings : GeoDataFrame GeoDataFrame containing buildings id_name : str name of the unique blocks id column to be generated unique_id : str name of the column with unique id. If there is none, it could be generated by unique_id(). This should be the same for cells and buildings, id's should match. Returns ------- buildings, cells, blocks : tuple buildings : GeoDataFrame GeoDataFrame containing buildings with added block ID cells : GeoDataFrame GeoDataFrame containing morphological tessellation with added block ID blocks : GeoDataFrame GeoDataFrame containing generated blocks """ cells_copy = cells.copy() print('Buffering streets...') street_buff = streets.copy() street_buff['geometry'] = streets.buffer(0.1) print('Generating spatial index...') streets_index = street_buff.sindex print('Difference...') cells_geom = cells_copy.geometry new_geom = [] for ix, cell in tqdm(cells_geom.iteritems(), total=cells_geom.shape[0]): # find approximate matches with r-tree, then precise matches from those approximate ones possible_matches_index = list(streets_index.intersection(cell.bounds)) possible_matches = street_buff.iloc[possible_matches_index] new_geom.append(cell.difference(possible_matches.geometry.unary_union)) single_geom = [] print('Defining adjacency...') for p in new_geom: if p.type == 'MultiPolygon': for polygon in p: single_geom.append(polygon) else: single_geom.append(p) blocks_gdf = gpd.GeoDataFrame(geometry=gpd.GeoSeries(single_geom)) spatial_weights = Queen.from_dataframe(blocks_gdf, silence_warnings=True) patches = {} jID = 1 for idx, row in tqdm(blocks_gdf.iterrows(), total=blocks_gdf.shape[0]): # if the id is already present in courtyards, continue (avoid repetition) if idx in patches: continue else: to_join = [idx] # list of indices which should be joined together neighbours = [] # list of neighbours weights = spatial_weights.neighbors[ idx] # neighbours from spatial weights for w in weights: neighbours.append(w) # make a list from weigths for n in neighbours: while n not in to_join: # until there is some neighbour which is not in to_join to_join.append(n) weights = spatial_weights.neighbors[n] for w in weights: neighbours.append( w ) # extend neighbours by neighbours of neighbours :) for b in to_join: patches[b] = jID # fill dict with values jID = jID + 1 blocks_gdf['patch'] = blocks_gdf.index.map(patches) print('Defining street-based blocks...') blocks_single = blocks_gdf.dissolve(by='patch') blocks_single['geometry'] = blocks_single.buffer(0.1) print('Defining block ID...') # street based blocks_single[id_name] = None blocks_single[id_name] = blocks_single[id_name].astype('float') b_id = 1 for idx, row in tqdm(blocks_single.iterrows(), total=blocks_single.shape[0]): blocks_single.loc[idx, id_name] = b_id b_id = b_id + 1 print('Generating centroids...') buildings_c = buildings.copy() buildings_c['geometry'] = buildings_c.representative_point( ) # make centroids blocks_single.crs = buildings.crs print('Spatial join...') centroids_tempID = gpd.sjoin(buildings_c, blocks_single, how='left', op='intersects') tempID_to_uID = centroids_tempID[[unique_id, id_name]] print('Attribute join (tesselation)...') cells_copy = cells_copy.merge(tempID_to_uID, on=unique_id) print('Generating blocks...') blocks = cells_copy.dissolve(by=id_name) cells_copy = cells_copy.drop([id_name], axis=1) print('Multipart to singlepart...') blocks = multi2single(blocks) blocks['geometry'] = blocks.exterior uid = 1 for idx, row in tqdm(blocks.iterrows(), total=blocks.shape[0]): blocks.loc[idx, id_name] = uid uid = uid + 1 blocks.loc[idx, 'geometry'] = Polygon(row['geometry']) # if polygon is within another one, delete it sindex = blocks.sindex for idx, row in tqdm(blocks.iterrows(), total=blocks.shape[0]): possible_matches = list(sindex.intersection(row.geometry.bounds)) possible_matches.remove(idx) possible = blocks.iloc[possible_matches] for idx2, row2 in possible.iterrows(): if row['geometry'].within(row2['geometry']): blocks.loc[idx, 'delete'] = 1 if 'delete' in blocks.columns: blocks = blocks.drop(list(blocks.loc[blocks['delete'] == 1].index)) blocks_save = blocks[[id_name, 'geometry']] centroids_w_bl_ID2 = gpd.sjoin(buildings_c, blocks_save, how='left', op='intersects') bl_ID_to_uID = centroids_w_bl_ID2[[unique_id, id_name]] print('Attribute join (buildings)...') buildings = buildings.merge(bl_ID_to_uID, on=unique_id) print('Attribute join (tesselation)...') cells = cells.merge(bl_ID_to_uID, on=unique_id) print('Done') return (buildings, cells, blocks_save)
def finaliseMapping(boardMAP): ###Get contiguity neighbors for mainland MAP_W = Queen.from_dataframe(boardMAP, idVariable='HBCode') ##Hacky fix to link Fife and Lothian final_W = getForthBridge(boardMAP, MAP_W) return final_W
import os import libpysal import geopandas from libpysal.weights import Queen, Rook, KNN import matplotlib.pyplot as plt sys.path.append(os.path.abspath('..')) libpysal.examples.available() libpysal.examples.explain('mexico') pth = libpysal.examples.get_path("mexicojoin.shp") gdf = geopandas.read_file(pth) ax = gdf.plot(edgecolor='grey', facecolor='w') ax.set_axis_off() w_rook = Rook.from_dataframe(gdf) f, ax = w_rook.plot(gdf, ax=ax, edge_kws=dict(color='r', linestyle=':', linewidth=1), node_kws=dict(marker='')) ax.set_axis_off() gdf.head() w_queen = Queen.from_dataframe(gdf) plt.show() ax = gdf.plot(edgecolor='grey', facecolor='w') f, ax = w_queen.plot(gdf, ax=ax, edge_kws=dict(color='r', linestyle=':', linewidth=1), node_kws=dict(marker='')) ax.set_axis_off()
def __init__(self): self.data = gpd.GeoDataFrame(Dataset('boston_housing').download(decode_geom=True))# gpd.read_file(self.filename) self.data.crs = {'init': 'epsg:4326'} self.w = Queen.from_dataframe(self.data)
def test_Alignment(self): self.df_buildings["orient"] = mm.Orientation(self.df_buildings).series sw = Queen.from_dataframe(self.df_tessellation, ids="uID") self.df_buildings["align_sw"] = mm.Alignment( self.df_buildings, sw, "uID", self.df_buildings["orient"]).series assert self.df_buildings["align_sw"][0] == 18.299481296455237
def test_NeighborDistance(self): sw = Queen.from_dataframe(self.df_tessellation, ids="uID") self.df_buildings["dist_sw"] = mm.NeighborDistance( self.df_buildings, sw, "uID").series check = 29.18589019096464 assert self.df_buildings["dist_sw"][0] == check
def _spatial_information_theory(data, group_pop_var, total_pop_var, w=None, unit_in_local_env=True, original_crs={'init': 'epsg:4326'}): """ Calculation of Spatial Information Theory index Parameters ---------- data : a geopandas DataFrame with a geometry column. group_pop_var : string The name of variable in data that contains the population size of the group of interest total_pop_var : string The name of variable in data that contains the total population of the unit w : W A PySAL weights object. If not provided, Queen contiguity matrix is used. This is used to construct the local environment around each spatial unit. unit_in_local_env : boolean A condition argument that states if the local environment around the unit comprises the unit itself. Default is True. original_crs : the original crs code given by a dict of data, but this is later be projected for the Mercator projection (EPSG = 3395). This argument is also to avoid passing data without crs and, therefore, raising unusual results. This index rely on the population density and we consider the area using squared kilometers. Attributes ---------- statistic : float Spatial Information Theory Index core_data : a geopandas DataFrame A geopandas DataFrame that contains the columns used to perform the estimate. Notes ----- Based on Reardon, Sean F., and David O’Sullivan. "Measures of spatial segregation." Sociological methodology 34.1 (2004): 121-162. This measure can be extended to a society with more than two groups. """ if (str(type(data)) != '<class \'geopandas.geodataframe.GeoDataFrame\'>'): raise TypeError( 'data is not a GeoDataFrame and, therefore, this index cannot be calculated.' ) if ((type(group_pop_var) is not str) or (type(total_pop_var) is not str)): raise TypeError('group_pop_var and total_pop_var must be strings') if ((group_pop_var not in data.columns) or (total_pop_var not in data.columns)): raise ValueError( 'group_pop_var and total_pop_var must be variables of data') if ('geometry' not in data.columns): data['geometry'] = data[data._geometry_column_name] data = data.drop([data._geometry_column_name], axis=1) data = data.set_geometry('geometry') if w is None: w_object = Queen.from_dataframe(data) else: w_object = w if (not issubclass(type(w_object), libpysal.weights.W)): raise TypeError('w is not a PySAL weights object') data = data.rename(columns={ group_pop_var: 'group_pop_var', total_pop_var: 'total_pop_var' }) data['compl_pop_var'] = data['total_pop_var'] - data['group_pop_var'] # In this case, M = 2 according to Reardon, Sean F., and David O’Sullivan. "Measures of spatial segregation." Sociological methodology 34.1 (2004): 121-162. pi_1 = data['group_pop_var'].sum() / data['total_pop_var'].sum() pi_2 = data['compl_pop_var'].sum() / data['total_pop_var'].sum() E = -1 * (pi_1 * math.log(pi_1, 2) + pi_2 * math.log(pi_2, 2)) T = data['total_pop_var'].sum() # Here you reproject the data using the Mercator projection data.crs = original_crs data = data.to_crs(crs={'init': 'epsg:3395'}) # Mercator sqm_to_sqkm = 10**6 data['area_sq_km'] = data.area / sqm_to_sqkm tau_p = data['total_pop_var'] / data['area_sq_km'] w_matrix = w_object.full()[0] if unit_in_local_env: np.fill_diagonal(w_matrix, 1) # The local context of each spatial unit is given by the aggregate context (this multiplication gives the local sum of each population) data['local_group_pop_var'] = np.matmul(data['group_pop_var'], w_matrix) data['local_compl_pop_var'] = np.matmul(data['compl_pop_var'], w_matrix) data['local_total_pop_var'] = np.matmul(data['total_pop_var'], w_matrix) pi_tilde_p_1 = np.array(data['local_group_pop_var'] / data['local_total_pop_var']) pi_tilde_p_2 = np.array(data['local_compl_pop_var'] / data['local_total_pop_var']) E_tilde_p = -1 * (pi_tilde_p_1 * np.log(pi_tilde_p_1) / np.log(2) + pi_tilde_p_2 * np.log(pi_tilde_p_2) / np.log(2)) SIT = 1 - 1 / (T * E) * (tau_p * E_tilde_p).sum( ) # This is the H_Tilde according to Reardon, Sean F., and David O’Sullivan. "Measures of spatial segregation." Sociological methodology 34.1 (2004): 121-162. core_data = data[['group_pop_var', 'total_pop_var', 'geometry']] return SIT, core_data
import sys import geopandas as gpd from libpysal.weights import Queen from ..greedy import greedy import pytest world = gpd.read_file(gpd.datasets.get_path("naturalearth_lowres")) sw = Queen.from_dataframe(world, ids=world.index.to_list(), silence_warnings=True) def test_default(): colors = greedy(world) assert len(colors) == len(world) assert set(colors) == set([0, 1, 2, 3, 4]) assert colors.value_counts().to_list() == [36, 36, 35, 35, 35] assert (colors.index == world.index).all() @pytest.mark.parametrize("pysal_geos", [None, 0]) def test_count(pysal_geos): colors = greedy( world, strategy="balanced", balance="count", min_distance=pysal_geos ) assert len(colors) == len(world) assert set(colors) == set([0, 1, 2, 3, 4]) assert colors.value_counts().to_list() == [36, 36, 35, 35, 35]
def alignment(objects, orientations, tessellation, unique_id, spatial_weights=None): """ Calculate the mean deviation of solar orientation of objects on adjacent cells from an object .. math:: \\frac{1}{n}\\sum_{i=1}^n dev_i=\\frac{dev_1+dev_2+\\cdots+dev_n}{n} Parameters ---------- objects : GeoDataFrame GeoDataFrame containing objects to analyse orientations : str, list, np.array, pd.Series the name of the dataframe column, np.array, or pd.Series where is stored object orientation value (can be calculated using :py:func:`momepy.orientation`) tessellation : GeoDataFrame GeoDataFrame containing morphological tessellation - source of weights_matrix. It is crucial to use exactly same input as was used during the calculation of weights matrix. If weights_matrix is None, tessellation is used to calulate it. unique_id : str the name of the dataframe column with unique id shared between a cell and a building (must be present in both geodataframes) spatial_weights : libpysal.weights, optional spatial weights matrix - If None, Queen contiguity matrix will be calculated based on tessellation Returns ------- Series Series containing resulting values. Examples -------- >>> buildings_df['alignment'] = momepy.alignment(buildings_df, 'bl_orient', tessellation_df, 'uID') Calculating alignments... Calculating spatial weights... Spatial weights ready... 100%|██████████| 144/144 [00:01<00:00, 140.84it/s] Alignments calculated. >>> buildings_df['alignment'][0] 18.299481296455237 """ # define empty list for results results_list = [] if not isinstance(orientations, str): objects['mm_o'] = orientations orientations = 'mm_o' print('Calculating alignments...') if not all(tessellation.index == range(len(tessellation))): raise ValueError( 'Index is not consecutive range 0:x, spatial weights will not match objects.' ) if spatial_weights is None: print('Calculating spatial weights...') from libpysal.weights import Queen spatial_weights = Queen.from_dataframe(tessellation) print('Spatial weights ready...') # iterating over rows one by one for index, row in tqdm(objects.iterrows(), total=objects.shape[0]): uid = tessellation.loc[tessellation[unique_id] == row[unique_id]].index[0] neighbours = spatial_weights.neighbors[uid] neighbours_ids = [] for n in neighbours: uniq = tessellation.iloc[n][unique_id] neighbours_ids.append(uniq) orientation = [] for i in neighbours_ids: ori = objects.loc[objects[unique_id] == i].iloc[0][orientations] orientation.append(ori) deviations = [] for o in orientation: dev = abs(o - row[orientations]) deviations.append(dev) if deviations: results_list.append(statistics.mean(deviations)) else: results_list.append(0) series = pd.Series(results_list) if 'mm_o' in objects.columns: objects.drop(columns=['mm_o'], inplace=True) print('Alignments calculated.') return series