Identifying Areas of Interest with Hotspots, Coldspots and Quadrant Maps

Choropleths are useful tools for helping us to understand how a variable, like deprication, demand or travel times, vary across a region.

However, it can be tricky to pick apart what matters, especially when you have multiple competing priorities.

Hotspots, coldspots, spatial outliers and quadrant maps can help you to zoom in on the areas that might need intervention or that might affect how good a solution is in practice, like areas of high deprivation coupled with poor access, or areas with high demand and high deprivation.

Lokigi provides a number of helper methods that can work with both Problem and SolutionSet classes to help highlight the areas of most and least concern.

For a detailed introduction to hotspots, take a look at the slides from the HSMA masterclass here.


To start, we can set up our problem as normal.

We’ll also import matplotlib to help us with compositing multiple plots together later.

from lokigi.site import SiteProblem
import matplotlib.pyplot as plt
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"
    )

problem.add_equity_data(
    "../../../sample_data/Index_of_Multiple_Deprivation_(Dec_2015)_Lookup_in_England.csv",
    equity_col="IMD15",
    common_col="LSOA11NM",
    label="Indices of Multiple Deprivation",
    continuous_measure=True,
    reverse=False
    )

We can now use the .get_hotspots() helper. First, we’ll take a look at demand hotpots.

hotspots_df = problem.get_hotspots(what="demand")
/__w/lokigi/lokigi/lokigi/mixins/site_eda.py:144: FutureWarning:

`use_index` defaults to False but will default to True in future. Set True/False directly to control this behavior and silence this warning

We can see that it’s returned a number of additional values, like the local Moran’s I, as well as evaluating whether it’s a significant pattern.

hotspots_df
FID LSOA11CD LSOA11NM LSOA11NMW BNG_E BNG_N LONG LAT GlobalID geometry LSOA demand local_moran_i p_value quadrant cluster_type
0 16356 E01016849 Brighton and Hove 026A Brighton and Hove 026A 529362 104513 -0.16464 50.82570 2e95f9d0-9795-4cd4-83d1-3a40898b6c89 POLYGON ((529551.999 104581, 529547.002 104555... Brighton and Hove 026A 1843 -0.010251 0.445 2 Not Significant
1 16357 E01016850 Brighton and Hove 029A Brighton and Hove 029A 529926 104360 -0.15669 50.82420 74a1b81a-ffc0-4c96-82ef-c63e7ec28eaf POLYGON ((530066.027 104493.355, 529958.177 10... Brighton and Hove 029A 2433 0.144585 0.210 1 Not Significant
2 16358 E01016851 Brighton and Hove 029B Brighton and Hove 029B 529674 104404 -0.16025 50.82465 a24f39b6-85f2-4ac6-8af8-f8d205b14b58 POLYGON ((529453.266 104354.713, 529547.002 10... Brighton and Hove 029B 2041 0.039325 0.036 1 Hotspot
3 16359 E01016852 Brighton and Hove 026B Brighton and Hove 026B 529680 104641 -0.16008 50.82678 9d674ffc-b718-4555-b669-0a5e6dc98129 POLYGON ((529787.909 104744.63, 529784.999 104... Brighton and Hove 026B 2540 0.504256 0.078 1 Not Significant
4 16360 E01016853 Brighton and Hove 026C Brighton and Hove 026C 529443 104770 -0.16340 50.82799 7087f545-a012-4c31-afdb-73df930edc2b POLYGON ((529461.706 104928.879, 529663.936 10... Brighton and Hove 026C 2233 -0.161983 0.086 4 Not Significant
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
160 16516 E01017010 Brighton and Hove 017D Brighton and Hove 017D 536120 105803 -0.06826 50.83575 e99d0ceb-67dc-4d7b-ab1f-ab8b58230e95 POLYGON ((536567.096 105821.386, 536031.682 10... Brighton and Hove 017D 1064 0.658483 0.133 3 Not Significant
161 16517 E01017011 Brighton and Hove 017E Brighton and Hove 017E 537345 105806 -0.05088 50.83549 d60da5d5-da96-4bed-a4e1-f93ffc876538 POLYGON ((537990.001 104635.399, 537733.25 105... Brighton and Hove 017E 2536 -0.295819 0.074 4 Not Significant
162 16518 E01017012 Brighton and Hove 017F Brighton and Hove 017F 535917 104786 -0.07152 50.82666 91575aee-ce23-436e-b85d-d0a94dbcef74 POLYGON ((536031.682 105223.601, 536625.001 10... Brighton and Hove 017F 1800 0.004076 0.493 3 Not Significant
163 32444 E01033328 Brighton and Hove 027F Brighton and Hove 027F 531208 104687 -0.13838 50.82685 f80f534f-bb85-49f8-9b67-730ecab28bad POLYGON ((531414.796 104753.739, 531358 104504... Brighton and Hove 027F 2323 0.296695 0.013 1 Hotspot
164 32445 E01033329 Brighton and Hove 027G Brighton and Hove 027G 530964 105059 -0.14171 50.83025 7e8f3fd7-9565-455b-9550-5fac52fcb7f4 POLYGON ((531226.813 104838.69, 530999 104730,... Brighton and Hove 027G 2780 -0.014248 0.477 4 Not Significant

165 rows × 16 columns

We’ll now plot our hotspots. We can see that there are some clusters of high demand near the coast, and some clusters of low demand elsewhere, as well as some outliers (e.g. an area of high demand surrounded by areas of low demand, and vice-versa).

problem.plot_hotspots(hotspots_df=hotspots_df, interactive=False)

We can hone in on just the hotspots by turning off the display of other categories.

problem.plot_hotspots(hotspots_df=hotspots_df, interactive=False, show_coldspots=False,
                      show_high_low_outliers=False,show_low_high_outliers=False,
                      show_non_significance=False)

We can also plot an interactive hotspot map, allowing us to interactively turn on and off the areas we are interested in.

problem.plot_hotspots(hotspots_df=hotspots_df, interactive=True)
Make this Notebook Trusted to load map: File -> Trust Notebook

Or we can turn them off in code to start the map up with certain areas hidden, though the option to turn them on still exists.

problem.plot_hotspots(hotspots_df=hotspots_df, interactive=True, show_coldspots=False,
                      show_high_low_outliers=False,show_low_high_outliers=False,
                      show_non_significance=False)
Make this Notebook Trusted to load map: File -> Trust Notebook

Equity

We can also use these plots for equity exploration as we have passed in equity data.

hotspots_equity_df = problem.get_hotspots(what="equity")

problem.plot_hotspots(hotspots_df=hotspots_equity_df, interactive=False)
/__w/lokigi/lokigi/lokigi/mixins/site_eda.py:358: UserWarning:

Using cached spatial weights.

We can combine both plots into a single plot with some additional matplotlib code.

# First we create blank subplots
fig, axes = plt.subplots(1, 2, figsize=(12, 5))

# Now we pass the relevant axis into our plot_hotspots function
problem.plot_hotspots(hotspots_df=hotspots_equity_df, interactive=False, ax=axes[0])
problem.plot_hotspots(hotspots_df=hotspots_df, interactive=False, ax=axes[1])

# Set the title for each
axes[0].set_title("Equity")
axes[1].set_title("Demand")

# Set an overall title
fig.suptitle("Hotspots", fontsize=16)

plt.tight_layout()
plt.show()

Let’s compare this with the raw underlying data.

# First we create blank subplots
fig, axes = plt.subplots(1, 2, figsize=(12, 5))

# Now we pass the relevant axis into our plot_hotspots function
problem.plot_region_geometry_layer(plot_equity=True, ax=axes[0], plot_region_of_interest_only=True)
problem.plot_region_geometry_layer(plot_demand=True, ax=axes[1])

# Set the title for each
axes[0].set_title("IMD Decile (Lower = More Deprived)")
axes[1].set_title("Demand")

# Set an overall title
fig.suptitle("Hotspots", fontsize=16)

plt.tight_layout()
plt.show()

And let’s just compare the hotpot plots to the raw plots too.

# First we create blank subplots
fig, axes = plt.subplots(1, 2, figsize=(12, 5))

# Now we pass the relevant axis into our plot_hotspots function
problem.plot_region_geometry_layer(plot_equity=True, ax=axes[0], plot_region_of_interest_only=True, cmap="Reds_r")
problem.plot_hotspots(hotspots_df=hotspots_equity_df, interactive=False, ax=axes[1])

# Set the title for each
axes[0].set_title("IMD Decile (Lower = More Deprived)")
axes[1].set_title("Equity Hotspots")

# Set an overall title
fig.suptitle("Hotspots", fontsize=16)

plt.tight_layout()
plt.show()

# First we create blank subplots
fig, axes = plt.subplots(1, 2, figsize=(12, 5))

# Now we pass the relevant axis into our plot_hotspots function
problem.plot_region_geometry_layer(plot_demand=True, ax=axes[0], cmap="RdBu_r")
problem.plot_hotspots(hotspots_df=hotspots_df, interactive=False, ax=axes[1])


# Set the title for each
axes[0].set_title("Raw Demand")
axes[1].set_title("Demand Hotspots")

# Set an overall title
fig.suptitle("Hotspots", fontsize=16)

plt.tight_layout()
plt.show()

Considering Demand and Deprivation Together

While these things are often useful to consider individually, they can be even more interesting to consider together.

We can run this work with a ‘what’ of ‘demand_equity’.

hotspots_combined_df = problem.get_hotspots(what="demand_equity")
hotspots_combined_df
/__w/lokigi/lokigi/lokigi/mixins/site_eda.py:358: UserWarning:

Using cached spatial weights.
FID LSOA11CD LSOA11NM LSOA11NMW BNG_E BNG_N LONG LAT GlobalID geometry ... demand IMD15 combined_score demand_level equity_level attribute_typology local_moran_i p_value quadrant cluster_type
0 16356 E01016849 Brighton and Hove 026A Brighton and Hove 026A 529362 104513 -0.16464 50.82570 2e95f9d0-9795-4cd4-83d1-3a40898b6c89 POLYGON ((529551.999 104581, 529547.002 104555... ... 1843 5 0.217900 Medium Medium Medium Demand / Medium Deprivation 0.045441 0.442 3 Not Significant
1 16357 E01016850 Brighton and Hove 029A Brighton and Hove 029A 529926 104360 -0.15669 50.82420 74a1b81a-ffc0-4c96-82ef-c63e7ec28eaf POLYGON ((530066.027 104493.355, 529958.177 10... ... 2433 4 0.358936 Medium Medium Medium Demand / Medium Deprivation 0.076936 0.198 1 Not Significant
2 16358 E01016851 Brighton and Hove 029B Brighton and Hove 029B 529674 104404 -0.16025 50.82465 a24f39b6-85f2-4ac6-8af8-f8d205b14b58 POLYGON ((529453.266 104354.713, 529547.002 10... ... 2041 5 0.245155 Medium Medium Medium Demand / Medium Deprivation -0.170309 0.036 2 Low-High Outlier
3 16359 E01016852 Brighton and Hove 026B Brighton and Hove 026B 529680 104641 -0.16008 50.82678 9d674ffc-b718-4555-b669-0a5e6dc98129 POLYGON ((529787.909 104744.63, 529784.999 104... ... 2540 3 0.439379 High High High Demand / High Deprivation 0.469248 0.089 1 Not Significant
4 16360 E01016853 Brighton and Hove 026C Brighton and Hove 026C 529443 104770 -0.16340 50.82799 7087f545-a012-4c31-afdb-73df930edc2b POLYGON ((529461.706 104928.879, 529663.936 10... ... 2233 5 0.271584 Medium Medium Medium Demand / Medium Deprivation 0.071052 0.077 3 Not Significant
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
160 16516 E01017010 Brighton and Hove 017D Brighton and Hove 017D 536120 105803 -0.06826 50.83575 e99d0ceb-67dc-4d7b-ab1f-ab8b58230e95 POLYGON ((536567.096 105821.386, 536031.682 10... ... 1064 8 0.044268 Low Low Low Demand / Low Deprivation 0.472135 0.252 3 Not Significant
161 16517 E01017011 Brighton and Hove 017E Brighton and Hove 017E 537345 105806 -0.05088 50.83549 d60da5d5-da96-4bed-a4e1-f93ffc876538 POLYGON ((537990.001 104635.399, 537733.25 105... ... 2536 2 0.501266 High High High Demand / High Deprivation -0.365030 0.125 4 Not Significant
162 16518 E01017012 Brighton and Hove 017F Brighton and Hove 017F 535917 104786 -0.07152 50.82666 91575aee-ce23-436e-b85d-d0a94dbcef74 POLYGON ((536031.682 105223.601, 536625.001 10... ... 1800 6 0.169585 Medium Low Medium Demand / Low Deprivation 0.035606 0.460 3 Not Significant
163 32444 E01033328 Brighton and Hove 027F Brighton and Hove 027F 531208 104687 -0.13838 50.82685 f80f534f-bb85-49f8-9b67-730ecab28bad POLYGON ((531414.796 104753.739, 531358 104504... ... 2323 3 0.397561 Medium High Medium Demand / High Deprivation 0.280910 0.030 1 Hotspot
164 32445 E01033329 Brighton and Hove 027G Brighton and Hove 027G 530964 105059 -0.14171 50.83025 7e8f3fd7-9565-455b-9550-5fac52fcb7f4 POLYGON ((531226.813 104838.69, 530999 104730,... ... 2780 4 0.416254 High Medium High Demand / Medium Deprivation 0.033883 0.391 1 Not Significant

165 rows × 21 columns

We can see this has created a new ‘attribute_typology’ column that classifies these into groups.

First, we’ll just plot the hotspots as before.

problem.plot_hotspots(hotspots_df=hotspots_combined_df, interactive=False)

We can also explore the same plot interactively.

problem.plot_hotspots(hotspots_df=hotspots_combined_df, interactive=True)
Make this Notebook Trusted to load map: File -> Trust Notebook

Let’s see our three pltos side by side.

# First we create blank subplots
fig, axes = plt.subplots(1, 3, figsize=(18, 6))

# Now we pass the relevant axis into our plot_hotspots function
problem.plot_hotspots(hotspots_df=hotspots_equity_df, interactive=False, ax=axes[0])
problem.plot_hotspots(hotspots_df=hotspots_df, interactive=False, ax=axes[1])
problem.plot_hotspots(hotspots_df=hotspots_combined_df, interactive=False, ax=axes[2])

# Set the title for each
axes[0].set_title("Equity")
axes[1].set_title("Demand")

# Set an overall title
fig.suptitle("Hotspots", fontsize=16)

plt.tight_layout()
plt.show()

Quadrant Plots

We can also run another type of plot that is specifically interested in the quadrants (or ninths).

By default, ninths are used as this produces a useful range of detail. Here, areas of both high demand and high deprivation - determined as being in the top third for both - are highlighted in a bright colour.

Medium demand and high deprivation, or high demand and medium deprivation, are classified as being of some concern, so are highlighted in yellow, and so on.

problem.plot_quadrant_map(figsize=(12, 7));
/__w/lokigi/lokigi/lokigi/mixins/site_eda.py:358: UserWarning:

Using cached spatial weights.

Alternatively, we can look at quadrants, where high demand = above average demand, and similarly for deprivation. However, this then gets combined with a measure of significance, so only areas of the most significance are highlighted.

problem.plot_quadrant_map(significance_threshold=0.1, figsize=(12, 7), n_bins=2);
/__w/lokigi/lokigi/lokigi/mixins/site_eda.py:358: UserWarning:

Using cached spatial weights.

Let’s now do an even more complex layout to combine two hotspot plots with a quadrant map.

# Create a 2x2 grid
fig = plt.figure(figsize=(16, 10))
gs = fig.add_gridspec(2, 2)

# Top row
ax1 = fig.add_subplot(gs[0, 0])
ax2 = fig.add_subplot(gs[0, 1])

# Bottom row spanning both columns
ax3 = fig.add_subplot(gs[1, :])

# Plot
problem.plot_hotspots(
    hotspots_df=hotspots_equity_df,
    interactive=False,
    ax=ax1,
)

problem.plot_hotspots(
    hotspots_df=hotspots_df,
    interactive=False,
    ax=ax2,
)

problem.plot_quadrant_map(ax=ax3)

# Titles
ax1.set_title("Equity")
ax2.set_title("Demand")

# Overall title
fig.suptitle("Hotspots", fontsize=16)

plt.tight_layout()
plt.show()
/__w/lokigi/lokigi/lokigi/mixins/site_eda.py:358: UserWarning:

Using cached spatial weights.

And we can repeat this for our 2-bin quadrant.

# Create a 2x2 grid
fig = plt.figure(figsize=(16, 10))
gs = fig.add_gridspec(2, 2)

# Top row
ax1 = fig.add_subplot(gs[0, 0])
ax2 = fig.add_subplot(gs[0, 1])

# Bottom row spanning both columns
ax3 = fig.add_subplot(gs[1, :])

# Plot
problem.plot_hotspots(
    hotspots_df=hotspots_equity_df,
    interactive=False,
    ax=ax1,
)

problem.plot_hotspots(
    hotspots_df=hotspots_df,
    interactive=False,
    ax=ax2,
)

problem.plot_quadrant_map(n_bins=2, significance_threshold=0.1, ax=ax3)

# Titles
ax1.set_title("Equity")
ax2.set_title("Demand")

# Overall title
fig.suptitle("Hotspots", fontsize=16)

plt.tight_layout()
plt.show()
/__w/lokigi/lokigi/lokigi/mixins/site_eda.py:358: UserWarning:

Using cached spatial weights.

Evaluating hotspots with our solution

Once we solve, we can identify another type of hotspot:

  • High deprivation and high travel times (and other associated quadrants or ninths like low deprivation and low travel times)
  • High demand and high travel times (and other associated quadrants or ninths like low demand and low travel times)
solution = problem.solve(p=3)
solution.plot_n_best_combinations(3);

As a reminder, we can access the solutions like this:

solution.show_solutions(n_best=3)
solution_rank site_names site_indices coverage_threshold weighted_average unweighted_average 90th_percentile max proportion_within_coverage_threshold problem_df
0 1 [Site 3, Site 4, Site 5] [2, 3, 4] None 5.37 5.45 8.50 16.69 0.0 LSOA     Site 3     Sit...
1 2 [Site 3, Site 4, Site 6] [2, 3, 5] None 5.38 5.36 8.06 16.69 0.0 LSOA     Site 3     Sit...
2 3 [Site 1, Site 3, Site 4] [0, 2, 3] None 5.53 5.67 9.36 16.69 0.0 LSOA     Site 1     Sit...

And we can then access individual detailed solution dataframes with per-region travel times like this:

solution.show_solutions(n_best=1)["problem_df"].iloc[0]
LSOA Site 3 Site 4 Site 5 min_cost selected_site within_threshold demand LSOA11NM IMD15 IMD15_raw
0 Brighton and Hove 027E 7.404833 8.197500 10.125667 7.404833 Site 3 NaN 3627 Brighton and Hove 027E 2 5040
1 Brighton and Hove 027F 8.626167 9.351167 9.649500 8.626167 Site 3 NaN 2323 Brighton and Hove 027F 3 9104
2 Brighton and Hove 027A 8.633000 6.840000 11.353833 6.840000 Site 4 NaN 2596 Brighton and Hove 027A 3 7483
3 Brighton and Hove 029E 11.006000 6.328667 12.193000 6.328667 Site 4 NaN 3132 Brighton and Hove 029E 2 6218
4 Brighton and Hove 029D 10.970000 5.216667 12.408333 5.216667 Site 4 NaN 2883 Brighton and Hove 029D 3 7018
... ... ... ... ... ... ... ... ... ... ... ...
160 Brighton and Hove 012A 18.468500 8.652667 10.433667 8.652667 Site 4 NaN 2497 Brighton and Hove 012A 3 8891
161 Brighton and Hove 005C 16.804000 9.490000 8.769167 8.769167 Site 5 NaN 2570 Brighton and Hove 005C 2 5951
162 Brighton and Hove 012B 18.876667 8.952500 10.841833 8.952500 Site 4 NaN 2051 Brighton and Hove 012B 3 8814
163 Brighton and Hove 005A 18.432167 11.068500 10.397333 10.397333 Site 5 NaN 1164 Brighton and Hove 005A 7 21094
164 Brighton and Hove 005B 17.232333 9.918333 9.197500 9.197500 Site 5 NaN 1097 Brighton and Hove 005B 7 20761

165 rows × 11 columns

As before, we can still plot our standard hotspot maps.

# Create a 2x2 grid
fig = plt.figure(figsize=(16, 8))
gs = fig.add_gridspec(2, 2)

# Top row
ax1 = fig.add_subplot(gs[0, 0])
ax2 = fig.add_subplot(gs[0, 1])

# Bottom row spanning both columns
ax3 = fig.add_subplot(gs[1, :])

# Plot
solution.plot_hotspots(
    interactive=False,
    what="equity",
    ax=ax1,
)

solution.plot_hotspots(
    what="demand",
    interactive=False,
    ax=ax2,
)

solution.plot_quadrant_map(ax=ax3)

# Titles
ax1.set_title("Equity")
ax2.set_title("Demand")

# Overall title
fig.suptitle("Hotspots", fontsize=16)

plt.tight_layout()
plt.show()
/__w/lokigi/lokigi/lokigi/mixins/site_eda.py:358: UserWarning:

Using cached spatial weights.

/__w/lokigi/lokigi/lokigi/mixins/site_eda.py:358: UserWarning:

Using cached spatial weights.

/__w/lokigi/lokigi/lokigi/mixins/site_eda.py:358: UserWarning:

Using cached spatial weights.

But we also have two more options available to us:

  • travel_equity
  • travel_demand
# Create a 2x2 grid
fig = plt.figure(figsize=(16, 8))
gs = fig.add_gridspec(2, 2)

# Top row
ax1 = fig.add_subplot(gs[0, 0])
ax2 = fig.add_subplot(gs[0, 1])

# Bottom row spanning both columns
ax3 = fig.add_subplot(gs[1, :])

solution.plot_best_combination(ax=ax1, cmap="RdBu_r")
problem.plot_region_geometry_layer(plot_equity=True, ax=ax2, plot_region_of_interest_only=True, cmap="Reds_r")

solution.plot_quadrant_map(what="travel_equity", ax=ax3)


# Titles
ax1.set_title("Travel Times")
ax2.set_title("Equity")

# Overall title
fig.suptitle("Hotspots", fontsize=16)

plt.tight_layout()
plt.show()
/__w/lokigi/lokigi/lokigi/mixins/site_eda.py:358: UserWarning:

Using cached spatial weights.

# Create a 2x2 grid
fig = plt.figure(figsize=(16, 8))
gs = fig.add_gridspec(2, 2)

# Top row
ax1 = fig.add_subplot(gs[0, 0])
ax2 = fig.add_subplot(gs[0, 1])

# Bottom row spanning both columns
ax3 = fig.add_subplot(gs[1, :])

solution.plot_best_combination(ax=ax1, cmap="RdBu_r")
problem.plot_region_geometry_layer(plot_demand=True, ax=ax2, plot_region_of_interest_only=True, cmap="Reds")
solution.plot_quadrant_map(what="travel_demand", ax=ax3)

# Titles
ax1.set_title("Travel Times")
ax2.set_title("Demand")

# Overall title
fig.suptitle("Hotspots", fontsize=16)

plt.tight_layout()
plt.show()
/__w/lokigi/lokigi/lokigi/mixins/site_eda.py:358: UserWarning:

Using cached spatial weights.

Repeating for a larger region

Note that in this dataset, we don’t have any demand.

Therefore, demand is set to be equal across all regions.

problem = SiteProblem()

problem.add_sites(
    "../../../sample_data/devon_mius.geojson",
    candidate_id_col="Facility_Name"
    )

problem.add_region_geometry_layer(
    "https://github.com/hsma-programme/h6_3c_interactive_plots_travel/raw/main/h6_3c_interactive_plots_travel/example_code/LSOA_2011_Boundaries_Super_Generalised_Clipped_BSC_EW_V4.geojson",
    common_col="LSOA11NM"
    )

problem.add_travel_matrix(
    travel_matrix_df="../../../sample_data/devon_miu_travel_matrix_public_transport_extended.csv",
    source_col="from_id",
    unit="minutes",
    )

problem.add_equity_data(
    "../../../sample_data/Index_of_Multiple_Deprivation_(Dec_2015)_Lookup_in_England.csv",
    equity_col="IMD15",
    common_col="LSOA11NM",
    label="Indices of Multiple Deprivation",
    continuous_measure=True,
    reverse=False
    )
# First we create blank subplots
fig, axes = plt.subplots(1, 2, figsize=(12, 5))

# Now we pass the relevant axis into our plot_hotspots function
problem.plot_region_geometry_layer(plot_equity=True, ax=axes[0], plot_region_of_interest_only=True, cmap="Reds_r")
problem.plot_hotspots(interactive=False, what="equity", ax=axes[1], significance_threshold=0.1)

# Set the title for each
axes[0].set_title("IMD Decile (Lower = More Deprived)")
axes[1].set_title("IMD Decile (Hotspots = Deprived, Coldspots = Less Deprived)")

# Set an overall title
fig.suptitle("Hotspots", fontsize=16)

plt.tight_layout()
plt.show()
/__w/lokigi/lokigi/lokigi/problem.py:415: UserWarning:

No demand data provided; estimating region of interest.

/__w/lokigi/lokigi/lokigi/mixins/site_eda.py:144: FutureWarning:

`use_index` defaults to False but will default to True in future. Set True/False directly to control this behavior and silence this warning

Given the larger number of small LSOAs in the more densely populated areas, this is where the interactive maps can prove more useful.

problem.plot_hotspots(interactive=True, what="equity", ax=axes[1], significance_threshold=0.1)
/__w/lokigi/lokigi/lokigi/mixins/site_eda.py:358: UserWarning:

Using cached spatial weights.
Make this Notebook Trusted to load map: File -> Trust Notebook

We’ll solve for a number of sites.

solution = problem.solve(p=3)

And we will look for hospots of high deprivation and high travel times.

# Create a 2x2 grid
fig = plt.figure(figsize=(14, 12))
gs = fig.add_gridspec(2, 2)

# Top row
ax1 = fig.add_subplot(gs[0, 0])
ax2 = fig.add_subplot(gs[0, 1])

# Bottom row spanning both columns
ax3 = fig.add_subplot(gs[1, :])

solution.plot_best_combination(ax=ax1, cmap="RdBu_r")
problem.plot_region_geometry_layer(plot_equity=True, ax=ax2, plot_region_of_interest_only=True, cmap="Reds_r")

solution.plot_quadrant_map(what="travel_equity", ax=ax3)

# Titles
ax1.set_title("Travel Times")
ax2.set_title("Equity")

# Overall title
fig.suptitle("Hotspots", fontsize=16)

plt.tight_layout()
plt.show()
/__w/lokigi/lokigi/lokigi/mixins/site_eda.py:358: UserWarning:

Using cached spatial weights.

We can also compare the effect of a 2x2 vs 3x3 plot.

# Create a 2x2 grid
fig = plt.figure(figsize=(20, 10), layout="constrained")
gs = fig.add_gridspec(1, 2)

# Top row
ax1 = fig.add_subplot(gs[0, 0])
ax2 = fig.add_subplot(gs[0, 1])

solution.plot_quadrant_map(what="travel_equity", ax=ax1, n_bins=2, significance_threshold=0.05, legend_loc="upper right")
solution.plot_quadrant_map(what="travel_equity", ax=ax2, n_bins=3, legend_loc="upper right")

ax1.set_title("Four bins with strict significance threshold: above median travel time = poor access")
ax2.set_title("Nine bins with strict significance threshold: above upper third of travel time = poor access")


plt.show()
/__w/lokigi/lokigi/lokigi/mixins/site_eda.py:358: UserWarning:

Using cached spatial weights.

/__w/lokigi/lokigi/lokigi/mixins/site_eda.py:358: UserWarning:

Using cached spatial weights.

And once again, we can opt for an interactive map.

solution.plot_quadrant_map(what="travel_equity", interactive=True)
/__w/lokigi/lokigi/lokigi/mixins/site_eda.py:358: UserWarning:

Using cached spatial weights.
Make this Notebook Trusted to load map: File -> Trust Notebook

We can also explore the underlying data.

df = solution.get_hotspots(what="travel_equity")
df
/__w/lokigi/lokigi/lokigi/mixins/site_eda.py:358: UserWarning:

Using cached spatial weights.
FID LSOA11CD LSOA11NM LSOA11NMW BNG_E BNG_N LONG LAT GlobalID geometry ... IMD15 _travel_norm combined_score equity_level access_level attribute_typology local_moran_i p_value quadrant cluster_type
0 14568 E01015023 Plymouth 003A Plymouth 003A 247552 60231 -4.14743 50.42208 3a1814a2-ceaf-47cd-8c29-72c900d3f53c POLYGON ((247909.169 60253.906, 248038.775 601... ... 2 0.073427 0.065268 High Good High Deprivation / Good Access -0.112181 0.277 2 Not Significant
1 14569 E01015024 Plymouth 004A Plymouth 004A 246294 60368 -4.16518 50.42299 5a713981-93f5-466a-b48f-d90e6216d857 POLYGON ((246946.495 60828.934, 246985.66 6080... ... 5 0.143357 0.079643 Medium Good Medium Deprivation / Good Access 0.088749 0.373 3 Not Significant
2 14570 E01015025 Plymouth 001A Plymouth 001A 249393 60200 -4.12152 50.42228 4764160d-79bd-450d-bc32-271f65347d50 POLYGON ((248598.949 60607.107, 249502.902 605... ... 9 0.048951 0.005439 Low Good Low Deprivation / Good Access 0.925685 0.045 3 Coldspot
3 14571 E01015026 Plymouth 003B Plymouth 003B 246634 60078 -4.16028 50.42047 bbcb84fc-7d49-433b-8e12-c66ef06c5982 POLYGON ((246915.514 59572.436, 246519.33 5983... ... 2 0.118881 0.105672 High Good High Deprivation / Good Access 0.183974 0.060 3 Not Significant
4 14572 E01015027 Plymouth 002A Plymouth 002A 248602 59865 -4.13251 50.41907 d308d90b-0dbb-4db1-bf09-f7667767a862 POLYGON ((249359.866 59750.857, 249064.408 593... ... 6 0.055944 0.024864 Medium Good Medium Deprivation / Good Access 0.011798 0.491 3 Not Significant
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
702 32366 E01033232 Exeter 011F Exeter 011F 295694 91608 -3.47881 50.71470 c8475b67-c262-4380-918e-9eeb1bb9fc23 POLYGON ((296030 91719.999, 295641.839 91301.8... ... 8 0.083916 0.018648 Low Good Low Deprivation / Good Access 1.035844 0.017 3 Coldspot
703 32367 E01033233 Exeter 001F Exeter 001F 291624 94781 -3.53737 50.74248 0fee6c51-0794-46fe-b5ea-ec792df399ac POLYGON ((292671.985 94446.666, 292666.919 943... ... 9 0.055944 0.006216 Low Good Low Deprivation / Good Access 0.993267 0.006 3 Coldspot
704 32368 E01033234 Exeter 011G Exeter 011G 296314 91965 -3.47013 50.71802 7d24b6bb-b0ec-4797-ac0a-7f8c1af362fd POLYGON ((296880.97 93135.099, 296759 91350.39... ... 9 0.108392 0.012044 Low Good Low Deprivation / Good Access 1.105920 0.002 3 Coldspot
705 32369 E01033235 Exeter 015E Exeter 015E 296469 88389 -3.46693 50.68590 899e780a-ef4c-420d-b3c0-f4d448d69e4b MULTIPOLYGON (((296254.69 87985.106, 296244.76... ... 9 0.111888 0.012432 Low Good Low Deprivation / Good Access -0.226004 0.281 2 Not Significant
706 32370 E01033236 Exeter 015F Exeter 015F 297029 87673 -3.45880 50.67956 c4999b02-c004-4d4d-a953-e9ab22f88a00 POLYGON ((297122.02 88128.081, 297203.6 87943.... ... 9 0.125874 0.013986 Low Good Low Deprivation / Good Access -0.067249 0.420 2 Not Significant

707 rows × 23 columns

df.groupby("attribute_typology")[["IMD15", "min_cost"]].mean().round(1)
IMD15 min_cost
attribute_typology
High Deprivation / Good Access 2.1 37.7
High Deprivation / Medium Access 2.5 81.0
High Deprivation / Poor Access 2.1 139.4
Low Deprivation / Good Access 8.3 33.8
Low Deprivation / Medium Access 8.3 81.8
Low Deprivation / Poor Access 7.9 135.8
Medium Deprivation / Good Access 4.9 32.3
Medium Deprivation / Medium Access 5.1 84.7
Medium Deprivation / Poor Access 5.0 153.6
Back to top