Source code for popframe.method.territory_evaluation

from typing import List
import networkx as nx
import geopandas as gpd
from shapely.geometry import Point, MultiPolygon
from shapely.ops import nearest_points
from itertools import combinations
import numpy as np
import pandas as pd


from ..models.region import Town
from .base_method import BaseMethod


[docs]class TerritoryEvaluation(BaseMethod): @classmethod def _is_criterion_satisfied(cls, profile_value, criterion_value): if isinstance(profile_value, tuple): return profile_value[0] <= criterion_value <= profile_value[1] return criterion_value >= profile_value @classmethod def _calculate_exceedance(cls, profile_value, criterion_value): if isinstance(profile_value, tuple): if profile_value[0] <= criterion_value <= profile_value[1]: return criterion_value - profile_value[0] return 0 return max(0, criterion_value - profile_value)
[docs] def calculate_potential(self, criteria_values): profiles = { "Жилая застройка - ИЖС": { "criteria": {"Население": 1, "Транспорт": 2, "Экология": 4, "Соц-об": 4, "Инж инф": 3}, "weights": {"Население": 0, "Транспорт": 0, "Экология": 1, "Соц-об": 1, "Инж инф": 0} }, "Жилая застройка - Малоэтажная": { "criteria": {"Население": 3, "Транспорт": 3, "Экология": 4, "Соц-об": 3, "Инж инф": 4}, "weights": {"Население": 1, "Транспорт": 1, "Экология": 1, "Соц-об": 1, "Инж инф": 1} }, "Жилая застройка - Среднеэтажная": { "criteria": {"Население": 4, "Транспорт": 4, "Экология": 4, "Соц-об": 3, "Инж инф": 5}, "weights": {"Население": 1, "Транспорт": 1, "Экология": 1, "Соц-об": 1, "Инж инф": 1} }, "Жилая застройка - Многоэтажная": { "criteria": {"Население": 5, "Транспорт": 5, "Экология": 4, "Соц-об": 3, "Инж инф": 5}, "weights": {"Население": 1, "Транспорт": 1, "Экология": 1, "Соц-об": 1, "Инж инф": 1} }, "Общественно-деловая": { "criteria": {"Население": 4, "Транспорт": 5, "Экология": 4, "Соц-об": 2, "Инж инф": 4}, "weights": {"Население": 1, "Транспорт": 1, "Экология": 1, "Соц-об": 0, "Инж инф": 1} }, "Рекреационная": { "criteria": {"Население": 0, "Транспорт": 0, "Экология": 4, "Соц-об": 0, "Инж инф": 0}, "weights": {"Население": 0, "Транспорт": 0, "Экология": 0, "Соц-об": 0, "Инж инф": 0} }, "Специального назначения": { "criteria": {"Население": 0, "Транспорт": 3, "Экология": 1, "Соц-об": 0, "Инж инф": 2}, "weights": {"Население": 1, "Транспорт": 1, "Экология": 1, "Соц-об": 0, "Инж инф": 1} }, "Промышленная": { "criteria": {"Население": 3, "Транспорт": 4, "Экология": 0, "Соц-об": 2, "Инж инф": 4}, "weights": {"Население": 1, "Транспорт": 1, "Экология": 0, "Соц-об": 0, "Инж инф": 1} }, "Сельско-хозяйственная": { "criteria": {"Население": 3, "Транспорт": 4, "Экология": 4, "Соц-об": 2, "Инж инф": 3}, "weights": {"Население": 1, "Транспорт": 1, "Экология": 1, "Соц-об": 0, "Инж инф": 1} }, "Транспортная инженерная": { "criteria": {"Население": 2, "Транспорт": 2, "Экология": 0, "Соц-об": 1, "Инж инф": 2}, "weights": {"Население": 0, "Транспорт": 0, "Экология": 0, "Соц-об": 0, "Инж инф": 0} } } potential_scores = {} for profile, data in profiles.items(): criteria = data["criteria"] weights = data["weights"] potential = sum( self._is_criterion_satisfied(criteria.get(criterion, -1), value) for criterion, value in criteria_values.items() ) weighted_score = sum( self._calculate_exceedance(criteria.get(criterion, -1), value) * weights.get(criterion, 1) for criterion, value in criteria_values.items() ) potential_scores[profile] = (potential, weighted_score) ranked_profiles = sorted(potential_scores.items(), key=lambda x: (x[0] != "Рекреационная", x[1][0], x[1][1]), reverse=True) ranked_profiles = [item for item in ranked_profiles if item[1][0] > 0] + [item for item in ranked_profiles if item[1][0] == 0] return ranked_profiles
[docs] def evaluate_territory_location(self, territories): """ Main function to evaluate the location of a territory relative to settlements. Parameters: self: The class object containing methods to retrieve settlements and territories. territories (GeoDataFrame): Geospatial data with territories. If None, territories from self.region will be used. Returns: list: A list of evaluation results for each territory. """ settlements_gdf = self.region.get_towns_gdf() territories_gdf = territories if territories is not None else self.region.get_territories_gdf() # Coordinate system transformation settlements_gdf = settlements_gdf.to_crs(epsg=3857) territories_gdf = territories_gdf.to_crs(epsg=3857) # Dictionary with settlement level scores level_scores = self._get_level_scores() results = [] # Process each territory for territory in territories_gdf.itertuples(): territory_name = getattr(territory, "name", "None") territory_geom = territory.geometry # Territory evaluation result = self._evaluate_single_territory(territory_geom, territory_name, settlements_gdf, level_scores) results.append(result) return results
[docs] def _get_level_scores(self): """ Returns a dictionary with scores for different levels of settlements. Returns: dict: A dictionary where the key is the settlement level and the value is the score. """ return { "Сверхкрупный город": 10, "Крупнейший город": 9, "Крупный город": 8, "Большой город": 7, "Средний город": 6, "Малый город": 5, "Крупное сельское поселение": 4, "Большое сельское поселение": 3, "Среднее сельское поселение": 2, "Малое сельское поселение": 1, }
[docs] def _evaluate_single_territory(self, territory_geom, territory_name, settlements_gdf, level_scores): """ Evaluates a single territory, determining its nearest settlements and interpretation. Parameters: territory_geom (geometry): The geometry of the territory. territory_name (str): The name of the territory. settlements_gdf (GeoDataFrame): Geospatial data with settlements. level_scores (dict): A dictionary with settlement level scores. Returns: dict: The results of the territory evaluation. """ buffer = territory_geom.buffer(10000) settlements_in_buffer = settlements_gdf[settlements_gdf.geometry.intersects(buffer)] if not settlements_in_buffer.empty: return self._evaluate_nearby_settlement(territory_name, settlements_in_buffer, level_scores) else: return self._evaluate_between_settlements(territory_geom, territory_name, settlements_gdf)
[docs] def _evaluate_nearby_settlement(self, territory_name, settlements_in_buffer, level_scores): """ Evaluates the territory if it is near settlements. Parameters: territory_name (str): The name of the territory. settlements_in_buffer (GeoDataFrame): The settlements within the buffer. level_scores (dict): A dictionary with settlement level scores. Returns: dict: The results of the territory evaluation. """ settlements_in_buffer['score'] = settlements_in_buffer['level'].map(level_scores) max_settlement = settlements_in_buffer.loc[settlements_in_buffer['score'].idxmax()] max_level = max_settlement['level'] max_score = max_settlement['score'] closest_settlement_name = max_settlement['name'] interpretation = f"Территория находится внутри или непосредственной близости населенного пункта уровня {max_level}" return { "territory": territory_name, "score": max_score, "interpretation": interpretation, "closest_settlement": closest_settlement_name, "closest_settlement1": None, "closest_settlement2": None }
[docs] def _evaluate_between_settlements(self, territory_geom, territory_name, settlements_gdf): """ Evaluates the territory if it is located between settlements. Parameters: territory_geom (geometry): The geometry of the territory. territory_name (str): The name of the territory. settlements_gdf (GeoDataFrame): Geospatial data with settlements. Returns: dict: The results of the territory evaluation. """ buffer_20km = territory_geom.buffer(30000) nearby_settlements = settlements_gdf[settlements_gdf.geometry.intersects(buffer_20km)] if len(nearby_settlements) > 1: closest_settlement1, closest_settlement2 = self._find_closest_settlement_pair(territory_geom, nearby_settlements) if closest_settlement1 and closest_settlement2: interpretation = "Территория находится между основными ядрами системы расселения" return { "territory": territory_name, "score": 1, "interpretation": interpretation, "closest_settlement": None, "closest_settlement1": closest_settlement1.name, "closest_settlement2": closest_settlement2.name } return { "territory": territory_name, "score": 0, "interpretation": "Территория находится за границей агломерации", "closest_settlement": None, "closest_settlement1": None, "closest_settlement2": None }
[docs] def _find_closest_settlement_pair(self, territory_geom, nearby_settlements): """ Finds the pair of nearest settlements around the territory. Parameters: territory_geom (geometry): The geometry of the territory. nearby_settlements (GeoDataFrame): The nearby settlements. Returns: tuple: The pair of nearest settlements and the minimum distance. """ from itertools import combinations min_distance = float('inf') closest_settlement1 = None closest_settlement2 = None for settlement1, settlement2 in combinations(nearby_settlements.itertuples(), 2): distance_between_settlements = settlement1.geometry.distance(settlement2.geometry) distance_to_settlement1 = territory_geom.distance(settlement1.geometry) distance_to_settlement2 = territory_geom.distance(settlement2.geometry) total_distance = distance_to_settlement1 + distance_to_settlement2 if (distance_to_settlement1 > 10000 and distance_to_settlement2 > 10000 and total_distance <= 1.2 * distance_between_settlements and total_distance < min_distance): min_distance = total_distance closest_settlement1 = settlement1 closest_settlement2 = settlement2 return closest_settlement1, closest_settlement2
[docs] def population_criterion(self, territories): """ Calculates population density and assesses territories based on demographic characteristics. Parameters: territories (GeoDataFrame or None): GeoDataFrame of territories or None to use default region territories. Returns: list: A list of dictionaries containing the assessment results for each territory. """ gdf_territory = self._get_territories_gdf(territories) towns_gdf = self.region.get_towns_gdf().to_crs(epsg=3857) results = self._calculate_density_population(gdf_territory, towns_gdf) for result in results: score = self._assess_territory(result['average_population_density'], result['total_population']) result['score'] = score result['interpretation'] = self._interpret_score(score) return results
[docs] def _get_territories_gdf(self, territories): """ Gets the GeoDataFrame of territories, transformed to CRS 3857. Parameters: territories (GeoDataFrame or None): GeoDataFrame of territories or None to use default region territories. Returns: GeoDataFrame: The territories GeoDataFrame transformed to CRS 3857. """ if territories is None: return self.region.get_territories_gdf().to_crs(epsg=3857) return territories.to_crs(epsg=3857)
[docs] def _calculate_density_population(self, gdf_territory, towns_gdf, radius_m=20000): """ Calculates population density within a specified buffer radius for each territory. Parameters: gdf_territory (GeoDataFrame): GeoDataFrame of territories. towns_gdf (GeoDataFrame): GeoDataFrame of towns with population data. radius_m (int, optional): Radius in meters for the buffer around each territory. Default is 20000. Returns: list: A list of dictionaries containing population density and total population for each territory. """ results = [] for _, territory in gdf_territory.iterrows(): buffer = territory.geometry.buffer(radius_m) towns_in_buffer = gpd.sjoin(towns_gdf, gpd.GeoDataFrame(geometry=[buffer], crs=towns_gdf.crs), op='intersects') if not towns_in_buffer.empty: total_population = towns_in_buffer['population'].sum() buffer_area = buffer.area / 1e6 # in square kilometers population_density = total_population / buffer_area if buffer_area > 0 else 0 else: total_population = 0 population_density = 0 results.append({ 'project': territory.get('name'), 'average_population_density': round(population_density, 1), 'total_population': total_population }) return results
[docs] def _assess_territory(self, density, population): """ Assesses the territory based on population density and total population. Parameters: density (float): The average population density of the territory. population (int): The total population of the territory. Returns: int: The score representing the assessment of the territory. """ if density == 0 and population == 0: return 0 score_df = pd.DataFrame([ {'min_dens': 0, 'max_dens': 10, 'min_pop': 0, 'max_pop': 1000, 'score': 1}, {'min_dens': 0, 'max_dens': 10, 'min_pop': 1000, 'max_pop': 5000, 'score': 2}, {'min_dens': 0, 'max_dens': 10, 'min_pop': 5000, 'max_pop': float('inf'), 'score': 3}, {'min_dens': 10, 'max_dens': 50, 'min_pop': 0, 'max_pop': 1000, 'score': 2}, {'min_dens': 10, 'max_dens': 50, 'min_pop': 1000, 'max_pop': 5000, 'score': 3}, {'min_dens': 10, 'max_dens': 50, 'min_pop': 5000, 'max_pop': float('inf'), 'score': 4}, {'min_dens': 50, 'max_dens': float('inf'), 'min_pop': 0, 'max_pop': 1000, 'score': 3}, {'min_dens': 50, 'max_dens': float('inf'), 'min_pop': 1000, 'max_pop': 5000, 'score': 4}, {'min_dens': 50, 'max_dens': float('inf'), 'min_pop': 5000, 'max_pop': float('inf'), 'score': 5} ]) result = score_df[ (score_df['min_dens'] <= density) & (density < score_df['max_dens']) & (score_df['min_pop'] <= population) & (population < score_df['max_pop']) ] if not result.empty: return result.iloc[0]['score'] return 0
[docs] def _interpret_score(self, score): """ Interprets the score assigned to a territory. Parameters: score (int): The score representing the assessment of the territory. Returns: str: The interpretation of the score. """ interpretations = { 0: "Территория имеет нулевые показатели численности и плотности населения, что может усложнить ее развитие.", 1: "Территория имеет малые показатели численности и плотности населения для активного развития.", 2: "Территория имеет умеренные показатели численности и плотности населения, что указывает на потенциал для развития.", 3: "Территория имеет показатели численности и плотности населения выше среднего, что указывает на возможность развития территории.", 4: "Территория имеет хорошие показатели численности и плотности населения, что способствует ее активному развитию.", 5: "Территория с высокими показателями численности и плотности населения, что указывает высокий потенциал развития." } return interpretations.get(score, "Неизвестный показатель.")