from lokigi.site import SiteProblem
import pandas as pdMinimizing the average travel time to sites
First, we need to import and initialise the SiteProblem class.
problem = SiteProblem()Let’s see what models are currently supported.
problem.describe_models(available_only=True)=== Supported Healthcare Location Models ===
ID: simple_p_median
Name: P-Median (Average Distance Minimizer)
Goal: Minimize the total travel distance or time.
When to use: Best for situations like siting a series of storage hubs efficiently across a region where demand/volume doesn't matter.
Main Trade-off: Can leave remote or rural patients with very long travel times in favor of urban density.
ID: p_median
Name: P-Median (Average Weighted Distance Minimizer)
Goal: Minimize the total travel distance or time for the entire population, weighting the travel times by a metric such as demand per region.
When to use: Best for general primary care where you want the 'average' patient to have the shortest trip possible.
Main Trade-off: Can leave remote or rural patients with very long travel times in favor of urban density.
ID: hybrid_simple_p_median
Name: Hybrid Simple P-Median (The Safety Net Model)
Goal: Minimize the total travel distance or time, weighting the travel times by a metric such as demand per region, while guaranteeing a maximum 'cutoff' time for every patient.
When to use: Makes the system efficient while preventing major 'postcode lotteries' where some patients travel a very long way.
Main Trade-off: Hard cut-off point could see some good solutions that nearly meet the cutoff being discarded.
ID: hybrid_p_median
Name: Hybrid P-Median (The Safety Net Model)
Goal: Minimize average travel distance or time while guaranteeing a maximum 'cutoff' time for every patient.
When to use: Makes the system efficient while preventing major 'postcode lotteries' where some patients travel a very long way.
Main Trade-off: Hard cut-off point could see some good solutions that nearly meet the cutoff being discarded.
ID: p_center
Name: P-Center (The Fair-Maximum Model)
Goal: Minimize the maximum travel distance or time for the furthest patient.
When to use: Best for ensuring universal access in rural regions; 'No one left behind'.
Main Trade-off: Can lead to higher average travel distances or times for the majority population.
ID: mclp
Name: Maximal Coverage Location Problem (MCLP)
Goal: Maximize the number of people within a specific time/distance 'threshold' (e.g., 15 minutes).
When to use: Best for emergency services (Ambulance/ER) where getting there within a 'Golden Hour' is more important than the average trip time.
Main Trade-off: Does not care how far away people are once they are outside the threshold.
To run a model, use: prob.solve_pmedian(p=3) or similar.
And what models will be supported in the future?
problem.describe_models(available_only=False)=== Healthcare Location Models ===
ID: simple_p_median
Name: P-Median (Average Distance Minimizer)
Goal: Minimize the total travel distance or time.
When to use: Best for situations like siting a series of storage hubs efficiently across a region where demand/volume doesn't matter.
Main Trade-off: Can leave remote or rural patients with very long travel times in favor of urban density.
Status: Supported
ID: p_median
Name: P-Median (Average Weighted Distance Minimizer)
Goal: Minimize the total travel distance or time for the entire population, weighting the travel times by a metric such as demand per region.
When to use: Best for general primary care where you want the 'average' patient to have the shortest trip possible.
Main Trade-off: Can leave remote or rural patients with very long travel times in favor of urban density.
Status: Supported
ID: hybrid_simple_p_median
Name: Hybrid Simple P-Median (The Safety Net Model)
Goal: Minimize the total travel distance or time, weighting the travel times by a metric such as demand per region, while guaranteeing a maximum 'cutoff' time for every patient.
When to use: Makes the system efficient while preventing major 'postcode lotteries' where some patients travel a very long way.
Main Trade-off: Hard cut-off point could see some good solutions that nearly meet the cutoff being discarded.
Status: Supported
ID: hybrid_p_median
Name: Hybrid P-Median (The Safety Net Model)
Goal: Minimize average travel distance or time while guaranteeing a maximum 'cutoff' time for every patient.
When to use: Makes the system efficient while preventing major 'postcode lotteries' where some patients travel a very long way.
Main Trade-off: Hard cut-off point could see some good solutions that nearly meet the cutoff being discarded.
Status: Supported
ID: p_center
Name: P-Center (The Fair-Maximum Model)
Goal: Minimize the maximum travel distance or time for the furthest patient.
When to use: Best for ensuring universal access in rural regions; 'No one left behind'.
Main Trade-off: Can lead to higher average travel distances or times for the majority population.
Status: Supported
ID: mclp
Name: Maximal Coverage Location Problem (MCLP)
Goal: Maximize the number of people within a specific time/distance 'threshold' (e.g., 15 minutes).
When to use: Best for emergency services (Ambulance/ER) where getting there within a 'Golden Hour' is more important than the average trip time.
Main Trade-off: Does not care how far away people are once they are outside the threshold.
Status: Supported
ID: lscp
Name: Location Set Covering Location Problem (LSCP)
Goal: Find the minimum number of facilities needed to cover *everyone* within a certain distance.
When to use: Best for universal mandates (e.g., ensuring every citizen is within 20 miles of a pharmacy).
Main Trade-off: Can be very expensive as it forces clinics into sparsely populated areas to reach the final 1%.
Status: Planned
ID: lscp-b
Name: LSCP-B (Backup Coverage Model)
Goal: Minimize the number of facilities needed to ensure everyone is covered by AT LEAST TWO locations.
When to use: Critical for high-reliability services like Maternity units or Stroke centers where 'System Busy' is a life-threatening risk.
Main Trade-off: Requires significantly more resources (budget/staff) than standard coverage to achieve the same geographic footprint.
Status: Planned
To run a model, use: prob.solve_pmedian(p=3) or similar.
Initialising required data
The show_ methods help us to see what format lokigi expects our data to be in.
problem.show_demand_format()
--- Expected Demand DataFrame Format ---
Note: Each row represents a unique demand location (e.g., LSOA).
site_id_col | demand_col
------------------------------
LSOA 1 | 25
LSOA 2 | 15
... | ...
----------------------------------------
problem.show_travel_format()
--- Expected Travel/Cost DataFrame Format ---
Note: Rows are sources, columns are destinations.
source_id | dest_1 | dest_2
--------------------------------------------------
source_1 | 22.6 | 16.3
source_2 | 15.1 | 17.1
... | ... | ...
--------------------------------------------
For example, if using LSOAs, your dataframe might look like this:
source_id | E01000259 | E01000314
--------------------------------------------------
Brighton and Hove 027E | 22.6 | 16.3
Brighton and Hove 005C | 15.1 | 17.1
... | ... | ...
--------------------------------------------
Or if you've defined your site names, it might look like this:
source_id | Site 1 | Site 1
--------------------------------------------------
Brighton and Hove 027E | 22.6 | 16.3
Brighton and Hove 005C | 15.1 | 17.1
... | ... | ...
--------------------------------------------
Add the required data
We can now use the various add_ methods to add in the required datasets.
Historical Demand
problem.add_demand("../../../sample_data/brighton_demand.csv", demand_col="demand", location_id_col="LSOA")problem.show_demand()| LSOA | demand | |
|---|---|---|
| 0 | Brighton and Hove 027E | 3627 |
| 1 | Brighton and Hove 027F | 2323 |
| 2 | Brighton and Hove 027A | 2596 |
| 3 | Brighton and Hove 029E | 3132 |
| 4 | Brighton and Hove 029D | 2883 |
| ... | ... | ... |
| 160 | Brighton and Hove 012A | 2497 |
| 161 | Brighton and Hove 005C | 2570 |
| 162 | Brighton and Hove 012B | 2051 |
| 163 | Brighton and Hove 005A | 1164 |
| 164 | Brighton and Hove 005B | 1097 |
165 rows × 2 columns
Candidate Sites
problem.add_sites("../../../sample_data/brighton_sites_named.geojson", candidate_id_col="site")problem.show_sites()| index | site | geometry | |
|---|---|---|---|
| 0 | 0 | Dahlia (Site 1) | POINT (527142.275 106616.053) |
| 1 | 1 | Begonia (Site 2) | POINT (531493.995 106639.488) |
| 2 | 2 | Tulip (Site 3) | POINT (533356.778 105476.782) |
| 3 | 3 | Daisy (Site 4) | POINT (528513.424 105052.43) |
| 4 | 4 | Daffodil (Site 5) | POINT (532421.163 109069.196) |
| 5 | 5 | Snowdrop (Site 6) | POINT (528716.452 108042.794) |
We can also plot the sites we’ve just put in.
problem.plot_sites()
problem.plot_sites(interactive=True)Make this Notebook Trusted to load map: File -> Trust Notebook
Travel Data
Our travel matrix is in seconds. Let’s update this to minutes when we load it in.
problem.add_travel_matrix(
travel_matrix_df="../../../sample_data/brighton_travel_matrix_driving_named.csv",
source_col="LSOA",
from_unit="seconds",
to_unit="minutes"
)problem.travel_matrix| LSOA | Dahlia (Site 1) | Begonia (Site 2) | Tulip (Site 3) | Daisy (Site 4) | Daffodil (Site 5) | Snowdrop (Site 6) | |
|---|---|---|---|---|---|---|---|
| 0 | Brighton and Hove 027E | 12.898833 | 8.794833 | 7.404833 | 8.197500 | 10.125667 | 9.248500 |
| 1 | Brighton and Hove 027F | 12.623167 | 8.318500 | 8.626167 | 9.351167 | 9.649500 | 8.972833 |
| 2 | Brighton and Hove 027A | 12.720667 | 10.023000 | 8.633000 | 6.840000 | 11.353833 | 9.289167 |
| 3 | Brighton and Hove 029E | 12.393667 | 10.862000 | 11.006000 | 6.328667 | 12.193000 | 9.293000 |
| 4 | Brighton and Hove 029D | 11.097500 | 11.077500 | 10.970000 | 5.216667 | 12.408333 | 9.508500 |
| ... | ... | ... | ... | ... | ... | ... | ... |
| 160 | Brighton and Hove 012A | 7.442333 | 14.745000 | 18.468500 | 8.652667 | 10.433667 | 7.463000 |
| 161 | Brighton and Hove 005C | 7.830000 | 13.080500 | 16.804000 | 9.490000 | 8.769167 | 5.798500 |
| 162 | Brighton and Hove 012B | 7.742167 | 15.153000 | 18.876667 | 8.952500 | 10.841833 | 7.871000 |
| 163 | Brighton and Hove 005A | 9.458167 | 14.708667 | 18.432167 | 11.068500 | 10.397333 | 7.426667 |
| 164 | Brighton and Hove 005B | 8.258333 | 13.508833 | 17.232333 | 9.918333 | 9.197500 | 6.226833 |
165 rows × 7 columns
Region Geometry
We’ll also want to add in a region geometry layer (a geodataframe containing the boundaries for the areas defined in our demand data) if we want to be able to plot some of our outputs.
problem.add_region_geometry_layer("https://github.com/hsma-programme/h6_3d_facility_location_problems/raw/refs/heads/main/h6_3d_facility_location_problems/example_code/LSOA_2011_Boundaries_Super_Generalised_Clipped_BSC_EW_V4.geojson", common_col="LSOA11NM")# problem.add_region_geometry_layer("../../../sample_data/LSOA_2011_Boundaries_Super_Generalised_Clipped_BSC_EW_V4.geojson", common_col="LSOA11NM")problem.show_region_geometry_layer()| FID | LSOA11CD | LSOA11NM | LSOA11NMW | BNG_E | BNG_N | LONG | LAT | GlobalID | geometry | |
|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1 | E01000001 | City of London 001A | City of London 001A | 532123 | 181632 | -0.097140 | 51.51816 | a758442e-7679-45d0-95a8-ed4c968ecdaa | POLYGON ((532282.629 181906.496, 532248.25 181... |
| 1 | 2 | E01000002 | City of London 001B | City of London 001B | 532480 | 181715 | -0.091970 | 51.51882 | 861dbb53-dfaf-4f57-be96-4527e2ec511f | POLYGON ((532746.814 181786.892, 532248.25 181... |
| 2 | 3 | E01000003 | City of London 001C | City of London 001C | 532239 | 182033 | -0.095320 | 51.52174 | 9f765b55-2061-484a-862b-fa0325991616 | POLYGON ((532293.068 182068.422, 532419.592 18... |
| 3 | 4 | E01000005 | City of London 001E | City of London 001E | 533581 | 181283 | -0.076270 | 51.51468 | a55c4c31-ef1c-42fc-bfa9-07c8f2025928 | POLYGON ((533604.245 181418.129, 533743.689 18... |
| 4 | 5 | E01000006 | Barking and Dagenham 016A | Barking and Dagenham 016A | 544994 | 184274 | 0.089317 | 51.53875 | 9cdabaa8-d9bd-4a94-bb3b-98a933ceedad | POLYGON ((545271.918 184183.948, 545296.314 18... |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 34748 | 34749 | W01001954 | Cardiff 006F | Caerdydd 006F | 312959 | 180574 | -3.255820 | 51.51735 | 5fc2d16e-8663-462f-936d-7535d0de1732 | POLYGON ((313011.929 181083.89, 313533.809 180... |
| 34749 | 34750 | W01001955 | Swansea 025F | Abertawe 025F | 265633 | 193182 | -3.942370 | 51.62137 | 0bcc9472-48d1-460c-a40e-c4a745269d84 | POLYGON ((266079.095 193572.406, 266140.774 19... |
| 34750 | 34751 | W01001956 | Swansea 023E | Abertawe 023E | 260586 | 192621 | -4.015000 | 51.61510 | 557e08ba-6aee-491d-8ba1-79eca916ce6b | POLYGON ((260107.578 194891.58, 260436.897 194... |
| 34751 | 34752 | W01001957 | Swansea 025G | Abertawe 025G | 265337 | 192555 | -3.946400 | 51.61567 | 43f945c4-e97d-4b1f-9e4d-46d154a6662e | POLYGON ((264991.859 192395.89, 264913.891 192... |
| 34752 | 34753 | W01001958 | Swansea 025H | Abertawe 025H | 266265 | 192630 | -3.933030 | 51.61656 | 84055fe9-868f-4150-9bea-082777cf132d | POLYGON ((266566.301 192259, 266577.9 191916.2... |
34753 rows × 10 columns
problem.plot_region_geometry_layer()
problem.plot_region_geometry_layer(plot_demand=True)
problem.plot_region_geometry_layer(interactive=True, height=600, width=600)Make this Notebook Trusted to load map: File -> Trust Notebook