Skip to content

Service Provision Analysis

This module evaluates how well services (e.g., schools, clinics) cover residential buildings based on their capacity and accessibility.
It models demand-supply relationships and provides tools to analyze and adjust service coverage.


Evaluate initial provision

Calculates provision scores between population points and service facilities considering:

  • Distance or time threshold,
  • Facility capacity,
  • Demand distribution.
objectnat.get_service_provision(buildings, adjacency_matrix, services, threshold, buildings_demand_column='demand', services_capacity_column='capacity')

Calculate load from buildings with demands on the given services using the distances matrix between them.

Parameters:

Name Type Description Default
services GeoDataFrame

GeoDataFrame of services

required
adjacency_matrix DataFrame

DataFrame representing the adjacency matrix

required
buildings GeoDataFrame

GeoDataFrame of demanded buildings

required
threshold int

Threshold value

required
buildings_demand_column str

column name of buildings demands

'demand'
services_capacity_column str

column name of services capacity

'capacity'

Returns:

Type Description
Tuple[GeoDataFrame, GeoDataFrame, GeoDataFrame]

Tuple of GeoDataFrames representing provision

GeoDataFrame

buildings, provision services, and provision links

Source code in src\objectnat\methods\provision\provision.py
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
def get_service_provision(
    buildings: gpd.GeoDataFrame,
    adjacency_matrix: pd.DataFrame,
    services: gpd.GeoDataFrame,
    threshold: int,
    buildings_demand_column: str = "demand",
    services_capacity_column: str = "capacity",
) -> Tuple[gpd.GeoDataFrame, gpd.GeoDataFrame, gpd.GeoDataFrame]:
    """Calculate load from buildings with demands on the given services using the distances matrix between them.

    Parameters:
        services (gpd.GeoDataFrame):
            GeoDataFrame of services
        adjacency_matrix (pd.DataFrame):
            DataFrame representing the adjacency matrix
        buildings (gpd.GeoDataFrame):
            GeoDataFrame of demanded buildings
        threshold (int):
            Threshold value
        buildings_demand_column (str):
            column name of buildings demands
        services_capacity_column (str):
            column name of services capacity

    Returns:
        (Tuple[gpd.GeoDataFrame, gpd.GeoDataFrame, gpd.GeoDataFrame]): Tuple of GeoDataFrames representing provision
        buildings, provision services, and provision links
    """
    buildings = buildings.copy()
    services = services.copy()
    adjacency_matrix = adjacency_matrix.copy()
    buildings["demand"] = buildings[buildings_demand_column]
    services["capacity"] = services[services_capacity_column]

    provision_buildings, provision_services, provision_links = Provision(
        services=services,
        demanded_buildings=buildings,
        adjacency_matrix=adjacency_matrix,
        threshold=threshold,
    ).run()
    return provision_buildings, provision_services, provision_links

service_provision_initial


Recalculate provision

Allows you to recalculate provision results with new accessibility thresholds without recomputing the full OD-matrix.

objectnat.recalculate_links(buildings, services, links, new_max_dist)
Source code in src\objectnat\methods\provision\provision.py
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
def recalculate_links(
    buildings: gpd.GeoDataFrame, services: gpd.GeoDataFrame, links: gpd.GeoDataFrame, new_max_dist: float
) -> tuple[gpd.GeoDataFrame, gpd.GeoDataFrame, gpd.GeoDataFrame]:
    buildings = buildings.copy()
    services = services.copy()
    links = links.copy()

    links_to_recalculate = links[links["distance"] > new_max_dist]
    if len(links_to_recalculate) == 0:
        logger.warning("To clip distance exceeds max links distance, returning full provision")
        return buildings, services, links

    links_to_keep = links[links["distance"] <= new_max_dist]
    free_demand = links_to_recalculate.groupby("building_index").agg({"demand": list, "distance": list})
    free_demand["distance"] = free_demand.apply(
        lambda x: sum((x1 * x2) for x1, x2 in zip(x.demand, x.distance)), axis=1
    )
    free_demand["demand"] = free_demand["demand"].apply(sum)
    free_demand = free_demand.reindex(buildings.index, fill_value=0)
    new_sum_time = (buildings["supplied_demands_within"] + buildings["supplied_demands_without"]) * buildings[
        "avg_dist"
    ] - free_demand["distance"]

    buildings["demand_left"] = buildings["demand_left"] + free_demand["demand"]
    buildings["supplied_demands_without"] = buildings["supplied_demands_without"] - free_demand["demand"]
    buildings["avg_dist"] = new_sum_time / (
        buildings["supplied_demands_without"] + buildings["supplied_demands_within"]
    )
    buildings["avg_dist"] = buildings.apply(
        lambda x: np.nan if (x["demand"] == x["demand_left"]) else round(x["avg_dist"], 2), axis=1
    )

    free_capacity = links_to_recalculate.groupby("service_index").agg({"demand": "sum"})
    free_capacity = free_capacity.reindex(services.index, fill_value=0)
    services["capacity_left"] = services["capacity_left"] + free_capacity["demand"]
    services["carried_capacity_without"] = services["carried_capacity_without"] - free_capacity["demand"]
    services["service_load"] = services["service_load"] - free_capacity["demand"]

    return buildings, services, links_to_keep

service_provision_recalculated


Clip to analysis area

Restricts provision output to a given geographic boundary (e.g., administrative area).

objectnat.clip_provision(buildings, services, links, selection_zone)
Source code in src\objectnat\methods\provision\provision.py
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
def clip_provision(
    buildings: gpd.GeoDataFrame, services: gpd.GeoDataFrame, links: gpd.GeoDataFrame, selection_zone: gpd.GeoDataFrame
) -> Tuple[gpd.GeoDataFrame, gpd.GeoDataFrame, gpd.GeoDataFrame]:

    assert selection_zone.crs == buildings.crs == services.crs == links.crs, (
        f"CRS mismatch: buildings_crs:{buildings.crs}, "
        f"links_crs:{links.crs} , "
        f"services_crs:{services.crs}, "
        f"selection_zone_crs:{selection_zone.crs}"
    )
    buildings = buildings.copy()
    links = links.copy()
    services = services.copy()

    s = buildings.intersects(selection_zone.union_all())
    buildings = buildings.loc[s[s].index]
    links = links[links["building_index"].isin(buildings.index.tolist())]
    services_to_keep = set(links["service_index"].tolist())
    services.drop(list(set(services.index.tolist()) - services_to_keep), inplace=True)
    return buildings, services, links

service_provision_clipped


Graph Preparation via IduEdu

For best performance and reproducibility, it is recommended to build or download intermodal graphs using the IduEdu library.

Here is a minimal example:

# Install required packages (uncomment if needed)
# !pip install iduedu

from iduedu import get_boundary, get_intermodal_graph

# Load boundary and graph for a specific region using OSM ID 1114252.
poly = get_boundary(osm_id=1114252)
G_intermodal = get_intermodal_graph(polygon=poly, clip_by_bounds=True)