Site location problems where certain sites must be included

Sometimes, we may have a series of sites that need to be included in any given solution. For example, you might have 3 existing sites and be looking to add one additional one while not closing any of the others.

Lokigi allows you to do this by specifying the sites that must be included when you add your sites to the SiteProblem object.

First, lets do our imports and set up a standard lokigi problem without any sites that must be included.

from lokigi.site import SiteProblem
import pandas as pd
problem = SiteProblem()
problem.add_demand("../../../sample_data/brighton_demand.csv", demand_col="demand", location_id_col="LSOA")
problem.add_sites("../../../sample_data/brighton_sites_existing.geojson", candidate_id_col="site")
problem.add_travel_matrix(
    travel_matrix_df="../../../sample_data/brighton_travel_matrix_driving.csv",
    source_col="LSOA",
    from_unit="seconds",
    to_unit="minutes"
    )
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")

We can solve this problem for 4 sites of 6 possible candidate locations, minimizing the weighted travel time in this case (a p-median problem).

solutions = problem.solve(p=4, objectives="p_median")
solutions.show_solutions()
site_names site_indices coverage_threshold weighted_average unweighted_average 90th_percentile max proportion_within_coverage_threshold problem_df
0 None [0, 2, 3, 4] None 5.08 5.12 7.82 16.69 0.0 LSOA                  L...
1 None [2, 3, 4, 5] None 5.11 5.05 7.42 16.69 0.0 LSOA                  L...
2 None [1, 2, 3, 5] None 5.20 5.07 7.75 16.69 0.0 LSOA                  L...
3 None [0, 1, 2, 3] None 5.22 5.21 8.34 16.69 0.0 LSOA                  L...
4 None [0, 2, 3, 5] None 5.23 5.19 8.06 16.69 0.0 LSOA                  L...
5 None [1, 2, 3, 4] None 5.28 5.33 8.32 16.69 0.0 LSOA                  L...
6 None [0, 2, 4, 5] None 6.05 5.90 9.07 16.69 0.0 LSOA                  L...
7 None [0, 1, 2, 5] None 6.14 5.92 9.33 16.69 0.0 LSOA                  L...
8 None [0, 1, 2, 4] None 6.24 6.17 9.69 16.69 0.0 LSOA                  L...
9 None [1, 2, 4, 5] None 6.33 6.17 9.26 16.69 0.0 LSOA                  L...
10 None [0, 1, 3, 4] None 6.63 6.43 11.32 21.71 0.0 LSOA                  L...
11 None [1, 3, 4, 5] None 6.66 6.37 11.32 21.71 0.0 LSOA                  L...
12 None [0, 1, 3, 5] None 6.83 6.48 11.58 22.86 0.0 LSOA                  L...
13 None [0, 3, 4, 5] None 6.91 6.56 12.26 21.71 0.0 LSOA                  L...
14 None [0, 1, 4, 5] None 7.67 7.27 11.67 21.71 0.0 LSOA                  L...

Let’s plot some of the best solutions.

solutions.plot_n_best_combinations(n_best=9, n_cols=3);

However, looking at our sites, we can see there’s another column this time that specifies whether the site must be included, and the version of our code above hasn’t taken that into account.

problem.show_sites()
index site existing geometry
0 0 Site 1 Y POINT (527142.275 106616.053)
1 1 Site 2 Y POINT (531493.995 106639.488)
2 2 Site 3 N POINT (533356.778 105476.782)
3 3 Site 4 N POINT (528513.424 105052.43)
4 4 Site 5 N POINT (532421.163 109069.196)
5 5 Site 6 N POINT (528716.452 108042.794)

So a lot of the solutions provided wouldn’t be valid.

Let’s reimport this sites data, but this time passing in a new parameter that lokigi supports - the required_sites_col parameter.

This will then look for common options in that column that could indicate that a site must be included in the final solution: "Yes", "yes", "Y", "y", True, true, or 1.

problem.add_sites("../../../sample_data/brighton_sites_existing.geojson",
                  candidate_id_col="site",
1                  required_sites_col="existing"
                  )
1
This is where we have indicated the new parameter.

Now we will solve again.

Tip

Note that we don’t need to specify anything different in our call to .solve() - it will automatically look for the required_sites_col if we’ve passed that in when setting up the problem.

solutions = problem.solve(p=4, objectives="p_median")
solutions.show_solutions()
site_names site_indices coverage_threshold weighted_average unweighted_average 90th_percentile max proportion_within_coverage_threshold problem_df
0 None [0, 1, 2, 3] None 5.22 5.21 8.34 16.69 0.0 LSOA                  L...
1 None [0, 1, 2, 5] None 6.14 5.92 9.33 16.69 0.0 LSOA                  L...
2 None [0, 1, 2, 4] None 6.24 6.17 9.69 16.69 0.0 LSOA                  L...
3 None [0, 1, 3, 4] None 6.63 6.43 11.32 21.71 0.0 LSOA                  L...
4 None [0, 1, 3, 5] None 6.83 6.48 11.58 22.86 0.0 LSOA                  L...
5 None [0, 1, 4, 5] None 7.67 7.27 11.67 21.71 0.0 LSOA                  L...

We can see that this time far fewer possible solutions have been passed back - only those that meet the criteria of having the specified sites included.

solutions.plot_n_best_combinations(n_cols=3);
/__w/lokigi/lokigi/lokigi/mixins/site_solution_plots.py:689: UserWarning: n_best parameter higher than number of available solutions. Returning 6 solutions.
  warn(

Because of the constraints provied, the best solution here (weighted average travel time of 5.2 minutes for sites 1, 2, 3 and 4) is very slightly worse than the unconstrained best solution (weighted average travel time of 5.1 minutes for sites 1, 3, 4 and 5).

In addition, we can see that the three worst solutions, which don’t include site 3, have a worse travel time for the LSOA in the bottom right hand corner, which is contributing to a significantly worse maximum travel time for those solutions.

Back to top