import geopandas as gpd
from pydantic import BaseModel, Field, field_validator
from shapely.geometry import Point
from typing import ClassVar
from ..models.geodataframe import GeoDataFrame, BaseRow
[docs]class TownRow(BaseRow):
"""
A model representing a town's data row in a GeoDataFrame.
Attributes
----------
geometry : shapely.geometry.Point
The geographic location of the town.
name : str
The name of the town.
population : int
The population of the town. Must be greater than zero.
level : str, optional
The administrative level of the town, defaults to "Нет уровня" (no level).
"""
geometry: Point
name: str
population: int = Field(gt=0)
level: str = Field(default="Нет уровня")
[docs]class LevelFiller(BaseModel):
"""
A class for automatically assigning administrative levels to towns based on population thresholds.
Attributes
----------
towns : GeoDataFrame[TownRow]
A GeoDataFrame containing town data that includes name, population, and level.
population_thresholds : dict
A class-level attribute defining population ranges for different administrative levels.
Methods
-------
_assign_level(row) -> str
A static method that assigns the correct administrative level to a town based on its population.
validate_towns(gdf)
A Pydantic validator that ensures town levels are correctly assigned before processing the GeoDataFrame.
fill_levels() -> GeoDataFrame[TownRow]
Fills in the levels for all towns in the GeoDataFrame based on population and returns the updated GeoDataFrame.
"""
towns: GeoDataFrame[TownRow]
population_thresholds: ClassVar[dict[str, tuple[int, int]]] = {
"Сверхкрупный город": (3000000, float('inf')),
"Крупнейший город": (1000000, 3000000),
"Крупный город": (250000, 1000000),
"Большой город": (100000, 250000),
"Средний город": (50000, 100000),
"Малый город": (5000, 50000),
"Крупное сельское поселение": (3000, 5000),
"Большое сельское поселение": (1000, 3000),
"Среднее сельское поселение": (200, 1000),
"Малое сельское поселение": (0, 200),
}
[docs] @staticmethod
def _assign_level(row) -> str:
"""
Assigns a level to a town based on its population.
Parameters
----------
row : pd.Series
A row from the GeoDataFrame containing town data.
Returns
-------
str
The administrative level of the town.
"""
population = row['population']
for level, (lower_bound, upper_bound) in LevelFiller.population_thresholds.items():
if lower_bound < population <= upper_bound:
return level
return "Неизвестный уровень" # If the population does not fit in any of the ranges
[docs] @field_validator("towns", mode="before")
@classmethod
def validate_towns(cls, gdf):
"""
Validates and assigns levels to towns in the provided GeoDataFrame.
Parameters
----------
gdf : GeoDataFrame
A GeoDataFrame containing town data.
Returns
-------
GeoDataFrame[TownRow]
A validated GeoDataFrame with assigned administrative levels.
"""
gdf["level"] = gdf.apply(cls._assign_level, axis=1)
return GeoDataFrame[TownRow](gdf)
[docs] def fill_levels(self) -> GeoDataFrame[TownRow]:
"""
Fills in the administrative levels for the towns based on their population.
Returns
-------
GeoDataFrame[TownRow]
An updated GeoDataFrame with filled levels for each town.
"""
towns = self.towns.copy()
towns["level"] = towns.apply(self._assign_level, axis=1)
return towns