Week3 Lab Tutorial Timeline

Time

Activity

16:00–16:05

Introduction — Overview of the main tasks for the lab tutorials

16:05–16:45

Tutorial: Static maps — Follow Section 3.1 of the Jupyter Notebook to practice the static maps

16:45–17:30

Tutorial: Interactive maps — Follow Section 3.2 of the Jupyter Notebook to practice the interactive maps using folium

17:30–17:55

Quiz — Complete quiz tasks

17:55–18:00

Wrap-up — Recap key points and address final questions

For this module’s lab tutorials, you can download all the required data using the provided link (click).

Please make sure that both the Jupyter Notebook file and the data and img folder are placed in the same directory (specifically within the STBDA_lab folder) to ensure the code runs correctly.

Week 3 Key Takeaways:

  • Create static maps and their elements (narrow arrow, scale bar, and legend) using Geopandas, Matplotlib, and Seaborn.

  • Create interactive maps using Folium and customize the maps using different functions and plugins in Folium.

  • Customize static and interactive maps for different geospatial types.

3 Visualisation for big geospatial data#

Principles of map visualization (cartography):

  • Map design: The process of creating maps that effectively communicate spatial information.

  • Map types: Different types of maps, such as dot, proportional, and choropleth maps.

  • Map interactivity: The ability to interact with maps through zooming, panning, and clicking on features.

  • Map elements: The components of a map, including title, legend, scale, and north arrow.

  • Map projections: The methods used to represent the curved surface of the Earth on a flat map.

3.1 Static maps#

We have used the gdf.plot() function in Geopandas and Matplotlb in the previous weeks to visualize geospatial data. This function is a simple way to create static maps,so we will use this function to visualize the geospatial data, i.e., mapping.

# Import the required libraries
import pandas as pd
import numpy as np
import geopandas as gpd
import seaborn as sns
sns.set_theme(style="ticks")
import matplotlib.pyplot as plt
import warnings
warnings.filterwarnings("ignore")

3.1.1 Dot map#

A dot map can be used to visualize the distribution of the point data. The dot map can directly show the density of the locations for a large number of points.

We have visualized the distribution of the Transport for London (TfL) Underground/Tube stations in last week, we add the TfL Bus Stop Locations in this section.

# Read the TfL Bus Stop Locations data
df_bus_stop = pd.read_csv("data/tfl_bus-stops.csv")
# the location coordinates are in the columns named 'Location_Easting' and 'Location_Northing' which are in the British National Grid (OSGB36) coordinate system.
df_bus_stop.head()
Stop_Code_LBSL Bus_Stop_Code Naptan_Atco Stop_Name Location_Easting Location_Northing Heading Stop_Area Virtual_Bus_Stop
0 1000 91532 490000266G WESTMINSTER STATION <> / PARLIAMENT SQUARE 530171.0 179738.0 177.0 0K08 0.0
1 10001 72689 490013793E TREVOR CLOSE 515781.0 174783.0 78.0 NB16 0.0
2 10002 48461 490000108F HIGHBURY CORNER 531614.0 184603.0 5.0 C902 0.0
3 10003 77150 490000108B HIGHBURY & ISLINGTON STATION <> # 531551.0 184803.0 127.0 C903 0.0
4 10004 48037 490012451S ST MARY MAGDALENE CHURCH 531365.0 184986.0 141.0 C904 0.0
# Convert the DataFrame to a GeoDataFrame
gdf_bus_stop = gpd.GeoDataFrame(df_bus_stop, geometry=gpd.points_from_xy(df_bus_stop['Location_Easting'], df_bus_stop['Location_Northing']))
# Set the coordinate reference system (CRS) to OSGB36
gdf_bus_stop.crs = "EPSG:27700"
# We also need the London boundary (we create this boundary by dissolve function in last week) to add the background of the map.
gdf_london_boundary = gpd.read_file("data/whole_london_boundary.geojson")
# transfer the coordinate system of the London boundary to OSGB36
gdf_london_boundary = gdf_london_boundary.to_crs("EPSG:27700")

Note that we don’t need the matplotlib if we only use the gdf.plot() to visualise one layer of geospatial data. But we need the matplotlib to plot multiple layers of geospatial data on the same map. As there is no inherent function for use to draw the scale bar and north arrow in Geopandas, so we need to use the other library to add the scale bar and north arrow on the map (You can find the instruction of scale bar and north arrow in this page(click)).

# Plot the bus stops and London boundary on a map
# Create a figure and axis
# NOTE: you MUST set the desired DPI here, when the subplots are created
# so that the scale_bar's DPI matches!
fig, ax = plt.subplots(figsize=(6, 6), dpi=300)
# Plot the London boundary
gdf_london_boundary.plot(ax=ax, # ax is the axis to plot on
                          color='lightgrey', # color of the polygon
                          edgecolor='black', # color of the boundary
                          alpha=0.3, # transparency of all colors
                          linewidth=0.5) # linewidth of the polygon boundary
# The bus stop points plot
gdf_bus_stop.plot(ax=ax, # another layer of points but put in the same axis
                  color='RoyalBlue', # color of the points
                  edgecolor='grey',  # color of the point boundary
                  linewidth=0.05, # linewidth of the point boundary
                  markersize=2, # size of the points
                  alpha=0.4, # transparency of the all colors
                  label='TfL Bus Stops', # label of the points
                   )
# We need the matplotlib_map_utils to add the scale bar and north arrow on the map
from matplotlib_map_utils.core.north_arrow import NorthArrow, north_arrow
from matplotlib_map_utils.core.scale_bar import ScaleBar, scale_bar
NorthArrow.set_size("small")
ScaleBar.set_size("small")
# Add the North Arrow and scale bar to the map
north_arrow(ax, location="upper right", rotation={"crs": 'EPSG:27700', "reference": 'center'})
scale_bar(ax, location="upper left", style="boxes", bar={"projection": 'EPSG:27700'})
# Set the title
ax.set_title('TfL Bus Stops in London')
# Set the legend
ax.legend(labels=['Bus Stops Locations'], loc='lower left', fontsize=8)
ax.set_xlabel('Easting (m)')
ax.set_ylabel('Northing (m)')
# We can save the figure as a jpg file
plt.savefig("fig/tfl_bus_stops.jpg", dpi=300, bbox_inches='tight')
plt.show()
_images/67ee9dd94f83287c9e46a619aa75ac03c30476fc084389f3e838ce0c7f71d433.png

From the map of TfL Bus Stops, we can see that the bus stops are more concentrated in the central London area, and the density of bus stops is lower in the outer London area.

Tailoring the dot map

We can tailor the dot map by changing the size of the dots, the color of the dots, and the transparency of the dots according to different attributes of the data (e.g., categorical data and numerical data).

  • Different colors represent different categories of the data (e.g., different types of transport).

  • Different sizes represent different values of the data (e.g., population size).

We will use the ‘NIGHT_TUBE’ column to tailor the dot map. The ‘NIGHT_TUBE’ column indicates whether the station is served by the Night Tube service or not. The Night Tube service is a 24-hour service on some London Underground lines, which allows passengers to travel at any time of the day or night. For selecting the colormap (cmap), you can refer to the Colormap from Matplotlib.

# Read the TfL Underground station geo-data
gdf_underground = gpd.read_file("data/Underground_Stations.geojson")
# transfer the coordinate system of the Underground stations to OSGB36
gdf_underground = gdf_underground.to_crs("EPSG:27700")
gdf_underground
OBJECTID NAME LINES ATCOCODE MODES ACCESSIBILITY NIGHT_TUBE NETWORK DATASET_LAST_UPDATED FULL_NAME geometry
0 111 St. Paul's Central 940GZZLUSPU bus, tube Not Accessible Yes London Underground 2021-11-29 00:00:00+00:00 St. Paul's station POINT Z (532108.364 181274.192 0)
1 112 Mile End District, Hammersmith & City, Central 940GZZLUMED bus, tube Partially Accessible - Interchange Only Yes London Underground 2021-11-29 00:00:00+00:00 Mile End station POINT Z (536500.796 182534.495 0)
2 113 Bethnal Green Central 940GZZLUBLG bus, tube Not Accessible Yes London Underground 2021-11-29 00:00:00+00:00 Bethnal Green station POINT Z (535043.555 182718.413 0)
3 114 Leyton Central 940GZZLULYN bus, tube Not Accessible Yes London Underground 2021-11-29 00:00:00+00:00 Leyton station POINT Z (538367.45 186075.857 0)
4 115 Snaresbrook Central 940GZZLUSNB bus, tube Not Accessible Yes London Underground 2021-11-29 00:00:00+00:00 Snaresbrook station POINT Z (540156.785 188804.046 0)
... ... ... ... ... ... ... ... ... ... ... ...
268 483 Seven Sisters Victoria 940GZZLUSVS tube Not Accessible Yes London Underground 2021-11-29 00:00:00+00:00 Seven Sisters station POINT Z (533642.911 188930.916 0)
269 484 Theydon Bois Central 940GZZLUTHB tube Partially Accessible No London Underground 2021-11-29 00:00:00+00:00 Theydon Bois station POINT Z (545541.525 199100.831 0)
270 485 Kenton Bakerloo 940GZZLUKEN tube Not Accessible No London Underground 2021-11-29 00:00:00+00:00 Kenton station POINT Z (516728.004 188314.476 0)
271 486 Woodside Park Northern 940GZZLUWOP tube, bus Fully Accessible No London Underground 2021-11-29 00:00:00+00:00 Woodside Park station POINT Z (525718.458 192588.13 0)
272 487 Preston Road Metropolitan 940GZZLUPRD tube, bus Not Accessible No London Underground 2021-11-29 00:00:00+00:00 Preston Road station POINT Z (518249.344 187281.833 0)

273 rows × 11 columns

# Create a figure and axis
fig, ax = plt.subplots(figsize=(8, 8), dpi=300)
# Plot the London boundary
gdf_london_boundary.plot(ax=ax, # ax is the axis to plot on
                          color='lightgrey', # color of the polygon
                          edgecolor='black', # color of the boundary
                          alpha=0.2, # transparency of all colors
                          linewidth=0.5) # linewidth of the polygon boundary
# The Underground stations plot
gdf_underground.plot(ax=ax,
                     column='NIGHT_TUBE', # column name to tailor the dot map, here is the categorical data
                     cmap='Set3', # color map
                      # note that we can use inherent legent=True to show the legend
                      # as we use the column name, we did not use it in the last map.
                      legend=True, # show the legend
                      legend_kwds={'loc': 'lower right', # legend position
                        'title': 'Night Tube', # legend title
                        'fontsize': 10, # legend title font size
                        'title_fontsize': 12, # legend title font size
                        'frameon': False, # legend frame
                                    }, # legend position
                      markersize=30, # size of the points
                      edgecolor='black', # color of the point boundary
                      alpha=1) # transparency of the all colors
# Add the North Arrow and scale bar to the map
north_arrow(ax, location="upper right", rotation={"crs": gdf_underground.crs, "reference": "center"})
scale_bar(ax, location="lower left", style="boxes", bar={"projection": gdf_underground.crs})
# Set the title
ax.set_title('Night Tube in TfL Underground Stations of London')
ax.set_xlabel('Easting (m)')
ax.set_ylabel('Northing (m)')
plt.savefig("fig/tfl_night_tube.jpg", dpi=300, bbox_inches='tight')
plt.show
<function matplotlib.pyplot.show(close=None, block=None)>
_images/aa8142697a796d2f813cf6bcd307baabd7336251ec8d9dd38cb5af2037352ed6.png

We will add usage information (counted by entry/exit) of London Underground stations to build a dot map. The usage data was published by TfL in the London Underground Station Crowding page. The usage data is in the CSV or XLSX format, and we can read it using the Pandas library.

# Read the 2023 London Underground station usage data
df_usage = pd.read_excel("data/TfL_AC2023_AnnualisedEntryExit.xlsx", header=5)
df_usage.head()
Unnamed: 0 Unnamed: 1 Unnamed: 2 Unnamed: 3 Unnamed: 4 Unnamed: 5 Monday Midweek (Tue-Thu) Friday Saturday Sunday Monday.1 Midweek (Tue-Thu).1 Friday.1 Saturday.1 Sunday.1 Weekly 12-week Annualised
0 Mode MNLC MASC Station Coverage Source Entries Entries Entries Entries Entries Exits Exits Exits Exits Exits En/Ex En/Ex En/Ex
1 LU 500 ACTu Acton Town Station entry/exit Gateline 7288 8125 8016 6552 4684 7546 8438 8245 6696 5224 103940 1188670 4823835
2 LU 502 ALDu Aldgate Station entry/exit Gateline 10183 12769 9612 6518 4860 11247 14494 11040 7982 5387 148617 1699609 6897314
3 LU 503 ALEu Aldgate East Station entry/exit Gateline 16046 19106 18158 17203 12549 15225 17758 17903 17163 11058 235895 2697738 10947896
4 LU 505 ALPu Alperton Station entry/exit Gateline 4187 4234 4294 3532 2487 4497 4539 4413 3682 2580 55992 640338 2598605
# we only need the London Underground stations and annualised usage, so we need to filter the data.
df_usage_lu = df_usage[df_usage['Unnamed: 0'] == 'LU'][['Unnamed: 3', 'Annualised']].rename(columns={'Unnamed: 3': 'name', 'Annualised': 'usage'})
df_usage_lu.head()
name usage
1 Acton Town 4823835
2 Aldgate 6897314
3 Aldgate East 10947896
4 Alperton 2598605
5 Amersham 1729521
# Then we combine the usage data with the TfL Underground stations data by the station name.
gdf_underground_usage = pd.merge(gdf_underground, df_usage_lu, left_on='NAME', right_on='name', how='left')
gdf_underground_usage
OBJECTID NAME LINES ATCOCODE MODES ACCESSIBILITY NIGHT_TUBE NETWORK DATASET_LAST_UPDATED FULL_NAME geometry name usage
0 111 St. Paul's Central 940GZZLUSPU bus, tube Not Accessible Yes London Underground 2021-11-29 00:00:00+00:00 St. Paul's station POINT Z (532108.364 181274.192 0) St. Paul's 9189088
1 112 Mile End District, Hammersmith & City, Central 940GZZLUMED bus, tube Partially Accessible - Interchange Only Yes London Underground 2021-11-29 00:00:00+00:00 Mile End station POINT Z (536500.796 182534.495 0) Mile End 11145822
2 113 Bethnal Green Central 940GZZLUBLG bus, tube Not Accessible Yes London Underground 2021-11-29 00:00:00+00:00 Bethnal Green station POINT Z (535043.555 182718.413 0) NaN NaN
3 114 Leyton Central 940GZZLULYN bus, tube Not Accessible Yes London Underground 2021-11-29 00:00:00+00:00 Leyton station POINT Z (538367.45 186075.857 0) Leyton 8592536
4 115 Snaresbrook Central 940GZZLUSNB bus, tube Not Accessible Yes London Underground 2021-11-29 00:00:00+00:00 Snaresbrook station POINT Z (540156.785 188804.046 0) Snaresbrook 1849887
... ... ... ... ... ... ... ... ... ... ... ... ... ...
268 483 Seven Sisters Victoria 940GZZLUSVS tube Not Accessible Yes London Underground 2021-11-29 00:00:00+00:00 Seven Sisters station POINT Z (533642.911 188930.916 0) Seven Sisters 12165434
269 484 Theydon Bois Central 940GZZLUTHB tube Partially Accessible No London Underground 2021-11-29 00:00:00+00:00 Theydon Bois station POINT Z (545541.525 199100.831 0) Theydon Bois 734300
270 485 Kenton Bakerloo 940GZZLUKEN tube Not Accessible No London Underground 2021-11-29 00:00:00+00:00 Kenton station POINT Z (516728.004 188314.476 0) Kenton 1515511
271 486 Woodside Park Northern 940GZZLUWOP tube, bus Fully Accessible No London Underground 2021-11-29 00:00:00+00:00 Woodside Park station POINT Z (525718.458 192588.13 0) Woodside Park 3657219
272 487 Preston Road Metropolitan 940GZZLUPRD tube, bus Not Accessible No London Underground 2021-11-29 00:00:00+00:00 Preston Road station POINT Z (518249.344 187281.833 0) Preston Road 2423452

273 rows × 13 columns

# Note that the usage columns have some missing values and some values are string '---' which means no data.
# You can use more codes to fill the data as NaN occurs as the mismatch of the name between the two datasets, though some cases are indicating one station.
print(gdf_underground_usage.usage.values)
[9189088 11145822 nan 8592536 1849887 6955728 1948236 4051381 3371643
 1167037 885823 4492296 267679 332284 nan 4383503 8491273 10787392 806675
 1503305 3937018 1212120 2153159 1860607 6314434 3063800 1597432 6030683
 6258859 1631696 1555062 1663555 1654538 2598605 4159381 13568222 27379635
 12573815 7822836 5106522 2203557 3293586 3947509 1450247 2307306 5962575
 7049636 nan 4377539 nan 14965619 6897314 nan 23330204 5185508 40070382
 72124262 11239346 21207073 nan nan 3644937 9413060 11353518 26088906
 2074444 1841679 5785361 3763523 5568858 7890670 nan 2624208 2899358 nan
 nan 7087668 nan 14076462 2645552 2542830 1623305 838918 3216718 1804676
 1952034 5748997 2138577 15779710 4373423 12110869 3172252 8590075
 37424555 1722368 3269238 5652447 5008989 nan 51106043 20240977 7803444
 9576731 6802053 nan 23172392 12328817 5573360 nan 5236551 7045026 5339416
 7137484 12397420 6265211 6323510 8040059 5210928 5113576 58726155 3889479
 2967011 4532637 '---' 4875982 5615724 4633070 3130908 1604937 1455300
 4909025 14142708 8975412 nan 6151983 1786001 2752421 2861021 397280
 1841035 3276268 2005688 3043893 3466311 3061427 820575 3540050 2385943
 17051267 2042489 1817060 2466317 4598994 1296040 nan 3937317 4045767
 26739879 nan nan 2689160 nan 2681574 2617813 nan 2297814 13156281 2717311
 nan 5022937 9088053 4266560 19172572 '---' nan nan 4774045 5338957
 13349683 15429088 4823835 1954910 28247401 20479785 7381051 8263631 nan
 2852360 2776023 3000487 3370726 1675267 2782422 5240900 6823150 5181211
 2134593 4663306 10947896 17452196 4157626 15510977 2565708 1989399 nan
 1057631 54376225 5419509 1373730 853803 1463521 1396078 1408690 1003523
 3340365 1398638 2223811 8435460 3353641 3002979 2073359 5870213 8508705
 nan 11223888 1296777 8399835 4108613 4040066 605628 4354076 nan 5347065
 4731235 4827763 nan 3529781 2547324 4780369 3727656 8875310 3019402
 9534074 7948900 15137045 3609039 1435853 5133701 3259745 2158123 1729521
 5426495 nan 18805872 nan 4321012 5198096 4408040 966889 1851241 808774
 2162088 837496 1653584 4067823 15846905 12258814 5179915 12165434 734300
 1515511 3657219 2423452]
# We need to convert the usage column to numeric and replace the '---' with NaN.
gdf_underground_usage['usage'] = gdf_underground_usage['usage'].replace('---', np.nan)
# Drop non-values
gdf_underground_usage_s = gdf_underground_usage[~gdf_underground_usage['usage'].isna()]
gdf_underground_usage_s
OBJECTID NAME LINES ATCOCODE MODES ACCESSIBILITY NIGHT_TUBE NETWORK DATASET_LAST_UPDATED FULL_NAME geometry name usage
0 111 St. Paul's Central 940GZZLUSPU bus, tube Not Accessible Yes London Underground 2021-11-29 00:00:00+00:00 St. Paul's station POINT Z (532108.364 181274.192 0) St. Paul's 9189088.0
1 112 Mile End District, Hammersmith & City, Central 940GZZLUMED bus, tube Partially Accessible - Interchange Only Yes London Underground 2021-11-29 00:00:00+00:00 Mile End station POINT Z (536500.796 182534.495 0) Mile End 11145822.0
3 114 Leyton Central 940GZZLULYN bus, tube Not Accessible Yes London Underground 2021-11-29 00:00:00+00:00 Leyton station POINT Z (538367.45 186075.857 0) Leyton 8592536.0
4 115 Snaresbrook Central 940GZZLUSNB bus, tube Not Accessible Yes London Underground 2021-11-29 00:00:00+00:00 Snaresbrook station POINT Z (540156.785 188804.046 0) Snaresbrook 1849887.0
5 116 Leytonstone Central 940GZZLULYS bus, tube Not Accessible Yes London Underground 2021-11-29 00:00:00+00:00 Leytonstone station POINT Z (539279.289 187404.74 0) Leytonstone 6955728.0
... ... ... ... ... ... ... ... ... ... ... ... ... ...
268 483 Seven Sisters Victoria 940GZZLUSVS tube Not Accessible Yes London Underground 2021-11-29 00:00:00+00:00 Seven Sisters station POINT Z (533642.911 188930.916 0) Seven Sisters 12165434.0
269 484 Theydon Bois Central 940GZZLUTHB tube Partially Accessible No London Underground 2021-11-29 00:00:00+00:00 Theydon Bois station POINT Z (545541.525 199100.831 0) Theydon Bois 734300.0
270 485 Kenton Bakerloo 940GZZLUKEN tube Not Accessible No London Underground 2021-11-29 00:00:00+00:00 Kenton station POINT Z (516728.004 188314.476 0) Kenton 1515511.0
271 486 Woodside Park Northern 940GZZLUWOP tube, bus Fully Accessible No London Underground 2021-11-29 00:00:00+00:00 Woodside Park station POINT Z (525718.458 192588.13 0) Woodside Park 3657219.0
272 487 Preston Road Metropolitan 940GZZLUPRD tube, bus Not Accessible No London Underground 2021-11-29 00:00:00+00:00 Preston Road station POINT Z (518249.344 187281.833 0) Preston Road 2423452.0

241 rows × 13 columns

# Plot the Underground stations with usage data
# Create a figure and axis
fig, ax = plt.subplots(figsize=(8, 8), dpi=300)
# Plot the London boundary
gdf_london_boundary.plot(ax=ax, # ax is the axis to plot on
                          color='lightgrey', # color of the polygon
                          edgecolor='black', # color of the boundary
                          alpha=0.2, # transparency of all colors
                          linewidth=0.5) # linewidth of the polygon boundary
# The Underground stations plot
gdf_underground_usage_s.plot(ax=ax,
                        column='usage', # column name to tailor the dot map, here is the numerical data
                        cmap='viridis', # color map
                        markersize=40, # size of the points
                        alpha=0.7, # transparency of the all colors
                        edgecolor='black', # color of the point boundary
                        linewidth=0.5, # linewidth of the point boundary
                        legend=True,
                        legend_kwds={ "label": "Annualised Usage",
                                    "orientation": "horizontal"
                                    })
# Add the North Arrow and scale bar to the map
north_arrow(ax, location="upper right", rotation={"crs": gdf_underground.crs, "reference": "center"})
scale_bar(ax, location="lower left", style="boxes", bar={"projection": gdf_underground.crs})
# Set the title
ax.set_title('Annualised Usage of TfL Underground Stations in London')
ax.set_xlabel('Easting (m)')
ax.set_ylabel('Northing (m)')
plt.savefig("fig/tfl_usage.jpg", dpi=300, bbox_inches='tight')
plt.show()
_images/81e3527b1ead550fa65b264c7ebe0db58ed659ad45e38fcb20e04b0f9f978a3b.png

3.1.2 Proportional map#

A proportional map is a variation of a dot map, where the size of the dots is proportional to the value of the variable being represented.

We can also use the size of the dot to represent the usage and use the colors to represent the different types of stations (like the night tube or not) of the Underground stations in this map. This means we bring three attributes of the data (locations, usage, type of night, or not) into one map. Note that in Geopandas gdf.plot(), only the legend=True only works automatically when you’re coloring your data by a column using the column= or color=. It does not create a legend for markersize if you pass a markersize argument like markersize=gdf_underground_usage_s.usage/100000 in the gdf.plot(). So we need to create a legend manually, or we can use another library like matplotlib or seaborn to help us.

# Plot the Underground stations with usage data
# Create a figure and axis
fig, ax = plt.subplots(figsize=(8, 8),dpi=300)
# Plot the London boundary
gdf_london_boundary.plot(ax=ax, # ax is the axis to plot on
                          color='lightgrey', # color of the polygon
                          edgecolor='black', # color of the boundary
                          alpha=0.2, # transparency of all colors
                          linewidth=0.5) # linewidth of the polygon boundary
# We use the seaborn to plot the Underground stations
sns.scatterplot(ax=ax,
                data=gdf_underground_usage_s,
                x=gdf_underground_usage_s.geometry.x, # x coordinate
                y=gdf_underground_usage_s.geometry.y, # y coordinate
                hue='NIGHT_TUBE', # column name to tailor the dot map, here is the categorical data
                size=gdf_underground_usage_s['usage'], # size of the points
                sizes=(0, 700), # size range of the point selection
                alpha=0.8, # transparency of the all colors
                palette='Set3', # color map
                linewidth=0.5, # linewidth of the point boundary
                edgecolor='black', # color of the point boundary
                legend=True,
                 )
# Add the North Arrow and scale bar to the map
north_arrow(ax, location="upper right", rotation={"crs": gdf_london_boundary.crs, "reference": "center"})
scale_bar(ax, location="lower right", style="boxes", bar={"projection": gdf_london_boundary.crs})
# Set the legend
handles, labels = ax.get_legend_handles_labels()
ax.legend(handles=handles,
          labels=['Night Tube', 'Yes', 'No', 'Annualised Usage', '15M', '30M', '45M', '60M'],
          loc='lower left', fontsize=10, title_fontsize=12, frameon=False)
# Set the title
ax.set_title('Annualised Usage of TfL Underground Stations in London')
ax.set_xlabel('Easting (m)')
ax.set_ylabel('Northing (m)')
plt.savefig("fig/tfl_usage_2.jpg", dpi=300, bbox_inches='tight')
plt.show()
_images/581ae5f5a385eb4099fd35da8d91df6c2026cd1bfb67c6eec71dda830343e4e4.png

3.1.3 Choropleth map#

A choropleth map is a type of thematic map where areas are shaded or patterned in proportion to the value of a variable being represented. It is used to visualize the spatial distribution of a variable across different regions (Polygons).

We use the UK LAs data to create a choropleth map according to different thematic topics.

# Read the UK LAs data
gdf_las = gpd.read_file("data/Local_Authority_Districts_December_2024_Boundaries_UK_BSC.geojson")
# Convert the coordinate reference system (CRS) to OSGB
gdf_las = gdf_las.to_crs("EPSG:27700")
gdf_las['area'] = gdf_las.geometry.area / 1e6 # convert the area to km2

We add a 2022 estimated resident population data of LAs from ONS (Office for National Statistics) to the gdf_las dat and create a choropleth map of population density distribution in the UK LAs.

# Read the 2022 population data
df_pop = pd.read_csv("data/populaiton_2022_all_ages_uk.csv")
df_pop.head()
Code Name Geography All ages
0 K02000001 UNITED KINGDOM Country 67596281
1 K03000001 GREAT BRITAIN Country 65685738
2 K04000001 ENGLAND AND WALES Country 60238038
3 E92000001 ENGLAND Country 57106398
4 E12000001 NORTH EAST Region 2683040
# merge the population data with the gdf_las data
gdf_las_pop = pd.merge(gdf_las, df_pop, left_on='LAD24CD', right_on='Code', how='left')
gdf_las_pop = gdf_las_pop.rename(columns={'All ages': 'population'})
# calculate the population density
gdf_las_pop['density'] = gdf_las_pop['population'] / gdf_las_pop['area'] # calculate the population density
# Plot the choropleth map of population distribution
# Create a figure and axis
fig, ax = plt.subplots(figsize=(8, 10), dpi=150)
# Plot the UK LAs
gdf_las_pop.plot(ax=ax, # ax is the axis to plot on
                  column='density', # column name to tailor the choropleth map, here is the numerical data
                  cmap='RdPu', # color map
                  edgecolor='black', # color of the polygon boundary
                  linewidth=0.1, # linewidth of the polygon boundary
                  alpha=1, # transparency of the all colors
                  legend=True,
                  legend_kwds={ "label": "Population density (per $km^2$)", # legend title
                                "orientation": "vertical", # legend orientation
                                "shrink": 0.6, # legend size shrink
                                },
                  )
# Add the North Arrow and scale bar to the map
north_arrow(ax, location="upper right", rotation={"crs": gdf_las_pop.crs, "reference": "center"})
scale_bar(ax, location="lower right", style="boxes", bar={"projection": gdf_las_pop.crs})
# Set the title
ax.set_title('Population Density Distribution in UK LAs')
ax.set_xlabel('Easting (m)')
ax.set_ylabel('Northing (m)')
plt.savefig("fig/uk_la_pop_den.jpg", dpi=300, bbox_inches='tight')
plt.show()
_images/903398d924fc563193c1afccdbb3fce4e53398f3dd71e03cfabd25a0e0c0492f.png

We have used a sequential color to plot the population distribution across the UK LAs. The darker the color, the higher the population. Alternatively, we can change the legend which means we can use the categorical data to plot the choropleth map. For example, we can classify the population density to several levels and plot the choropleth map by using setting scheme and k in the plot().

# Plot the choropleth map of population distribution
# Create a figure and axis
fig, ax = plt.subplots(figsize=(8, 10), dpi=300)
# Plot the UK LAs
gdf_las_pop.plot(ax=ax, # ax is the axis to plot on
                  column='density', # column name to tailor the choropleth map, here is the numerical data
                  scheme = 'Quantiles', # classification method
                  cmap='RdPu', # color map
                  edgecolor='black', # color of the polygon boundary
                  linewidth=0.1, # linewidth of the polygon boundary
                  alpha=1, # transparency of the all colors
                  legend=True,
                  legend_kwds={ "title": "Population density (per $km^2$)", # legend title
                                "fontsize":8, # legend label font size
                                "title_fontsize": 9, # legend title font size
                                "loc": "upper left", # legend position
                                "frameon": False,
                                    },
                  )
# Add the North Arrow and scale bar to the map
north_arrow(ax, location="upper right", rotation={"crs": gdf_las_pop.crs, "reference": "center"})
scale_bar(ax, location="lower right", style="boxes", bar={"projection": gdf_las_pop.crs})
# Set the title
ax.set_title('Population Density Distribution in UK LAs')
ax.set_xlabel('Easting (m)')
ax.set_ylabel('Northing (m)')
plt.savefig("fig/uk_la_pop_den_2.jpg", dpi=300, bbox_inches='tight')
plt.show()
_images/8d50bf71d64f226a10f8245278e401ef9a46ff5527fc8730a7c05d7cc2ddc2d8.png

We also use a pkg called contextily to add the basemap for the static map; the doc can be found at this page.

Note: We need to use Web Mercator (epsg=3857) to map the base map and gdf.

import contextily as cx
# Plot the choropleth map of population distribution
# Create a figure and axis
fig, ax = plt.subplots(figsize=(8, 10), dpi=300)

# Plot the UK LAs
gdf_las_pop = gdf_las_pop.to_crs(epsg=3857)
gdf_las_pop.plot(ax=ax, # ax is the axis to plot on
                  column='density', # column name to tailor the choropleth map, here is the numerical data
                  scheme = 'Quantiles', # classification method
                  cmap='RdPu', # color map
                  edgecolor='black', # color of the polygon boundary
                  linewidth=0.1, # linewidth of the polygon boundary
                  alpha=0.7, # transparency of the all colors
                  legend=True,
                  legend_kwds={ "title": "Population density (per $km^2$)", # legend title
                                "fontsize":8, # legend label font size
                                "title_fontsize": 9, # legend title font size
                                "loc": "upper left", # legend position
                                "frameon": False,
                                    },
                  )

# Add the basemap
cx.add_basemap(ax, source=cx.providers.CartoDB.Positron, zoom=6)

# Add the North Arrow and scale bar to the map
north_arrow(ax, location="upper right", rotation={"crs": gdf_las_pop.crs, "reference": "center"})
scale_bar(ax, location="lower right", style="boxes", bar={"projection": gdf_las_pop.crs})
# Set the title
ax.set_title('Population Density Distribution in UK LAs')
ax.set_xlabel('Easting (m)')
ax.set_ylabel('Northing (m)')
ax.axis('off')
plt.savefig("fig/uk_la_pop_den_map_base_map.jpg", dpi=300, bbox_inches='tight')
plt.show()
_images/a7188d592ac6cf80b73909369b9e8cf4d308158395b2e3db1155512602282e28.png

3.2 Interactive maps#

Interactive maps are maps that allow users to interact with the map by zooming, panning, and clicking on features. They are used to visualize geospatial data in a more dynamic way. There are several libraries in Python that can be used to create interactive maps, such as Folium, Plotly, and Bokeh.

We will use Folium to create interactive maps in this section. The Folium library is built on the Leaflet.js library, which is a popular JavaScript library for creating interactive maps. Folium allows us to create interactive maps using Python and provides a simple interface for adding layers, markers, and popups to the map. We continue to use the TfL Underground stations, Bus Stops, and UK population data to create interactive maps.

import folium

Note that we need to use the latitude and longitude coordinates, i.e., the WGS84 coordinate system (EPSG:4326) to create the map (Folium creat the html). Use the m, the jupyter widget, to show the map in the notebook. If you want to save the map as an HTML file, you can use the m.save() function.

# Create a map centered on London
# The basemap is provided by the OpenStreetMap.
m = folium.Map(location=[51.5074, -0.1278], # center of London
               tiles='OpenStreetMap', # basemap
               zoom_start=12) # initial zoom level
m
# m.save("fig/london_map.html")
Make this Notebook Trusted to load map: File -> Trust Notebook

Use m.save() to save the map as an HTML file and open with any browser app. If your computer ram is limited, then use m cannot show in jupyter-lab efficiently.

3.2.1 Circle and CircleMarker#

CircleMarker has a radius specified in pixels, while Circle is specified in meters. That means a CircleMarker will not change size on your screen when you zoom, while Circle will have a fixed position on the map. There are two different functions in folium to create the circle and circle marker.

  • For the data formats (e.g., numpy array, pandas) have the coordinates (x, y),

    • folium.Circle():

    • folium.CircleMarker():

  • Only for geopandas GeoDataFrame (gdf) with the geometry column. Check the documentation for more details:

    • folium.GeoJson(marker = folium.Circle()):

    • folium.GeoJson(marker = folium.CircleMarker()):

Now we use the london TfL Bus Stops (19k) to create an interactive map using folium.Cricle() and folium.GeoJson(marker = folium.Circle()).

# transfer the coordinate system of the London bus stops to WGS84
gdf_bus_stop = gdf_bus_stop.to_crs("EPSG:4326")
# select the columns we need
gdf_bus_stop_s = gdf_bus_stop[['geometry', 'Stop_Name']]
# drop the NaN values
gdf_bus_stop_s = gdf_bus_stop_s[~gdf_bus_stop_s['Stop_Name'].isna()]
print('The numbers of bus stops:', len(gdf_bus_stop_s))
The numbers of bus stops: 19790

folium.Circle()

# Create a map centered on London
m = folium.Map(location=[51.5074, -0.1278],
               tiles='OpenStreetMap',
               zoom_start=15)

# Loop through the DataFrame and add each bus stop using folium.Circle
for _, row in gdf_bus_stop_s.iterrows():
    location = [row.geometry.y, row.geometry.x]  # lat, lon
    folium.Circle(
        location=location,
        radius=25, # radius in meters
        fill=True,
        fill_color="royalblue", # fill color
        fill_opacity=0.8, # fill opacity
        color="black", # color of the circle boundary
        weight=0.1, # weight of the circle boundary
        tooltip=f"Bus Stop Name: {row['Stop_Name']}" # tooltip is the text when hovering the circle
    ).add_to(m)
# m.save("fig/london_bus_stops.html")
# m

folium.GeoJason(marker = folium.Circle())

Here, we use folium.GeoJason(marker = folium.Circle()) and change the basemap/tiles to Cartodb Positron to see the difference. (More tiles can be found in the documentation)

# Create a map centered on London
m = folium.Map(location=[51.5074, -0.1278], # center of London
               tiles='Cartodb Positron', # basemap
               zoom_start=15) # initial zoom level

# Add the TfL Bus Stops to the map
folium.GeoJson(data=gdf_bus_stop_s,
               marker=folium.Circle(radius=25,
                                    fill_color="royalblue",
                                    fill_opacity=0.8,
                                    color="black",
                                    weight=0.1),
               tooltip=folium.GeoJsonTooltip(fields=['Stop_Name'], # tooltip is the text when hovering the circle, field is the data column name
                                             aliases=['Bus Stop Name:'],  # aliases is the text when hovering the circle
                                             )
               ).add_to(m)

m.save("fig/london_bus_stops_2.html")
# m

folium.CircleMarker()

We use the CircleMarker to plot the TfL Underground stations with the usage data. The CircleMarker can be customized with different colors, sizes, and popups.

gdf_underground_usage_s = gdf_underground_usage_s.to_crs("EPSG:4326") # convert the coordinate system to WGS84
# Create a map centered on London
m = folium.Map(location=[51.5074, -0.1278], # center of London
                tiles='OpenStreetMap', # basemap
                zoom_start=13) # initial zoom level
# Add the TfL Underground stations to the map, we use the for loop to add the circle marker one by one
for i in range(len(gdf_underground_usage_s)):
    folium.CircleMarker(location=[gdf_underground_usage_s.geometry.y.iloc[i],
                                  gdf_underground_usage_s.geometry.x.iloc[i]], # location is the latitude and longitude
                        radius=gdf_underground_usage_s['usage'].iloc[i] / 1000000, # size of the circle according to the usage
                        color='royalblue', # color of the circle
                        fill=True, # fill the circle
                        fill_color='royalblue', # fill color of the circle
                        fill_opacity=0.7, # transparency of the fill color
                        weight=0.7, # weight of the circle boundary
                        popup=gdf_underground_usage_s.iloc[i]['usage'], # popup text: usage, when clicking the circle, it will show the station name.
                        tooltip=gdf_underground_usage_s.iloc[i]['FULL_NAME'], # tooltip text: station names, when hovering the circle, it will show the station name.
                        ).add_to(m)
m.save("fig/tfl_underground_stations.html")
# m

folium.GeoJson(marker = folium.CircleMarker())

Apart from the folium.CircleMarker(), Folium also provides function folium.GeoJson(marker=folium.CircleMarker())for geopandas dataframe directly.

gdf_underground_usage_s = gdf_underground_usage_s[['geometry', 'usage', 'LINES', 'FULL_NAME']]
# Create a map centered on London
m = folium.Map(location=[51.5074, -0.1278], # center of London
                tiles='Cartodb Positron', # basemap
                zoom_start=13) # initial zoom level
# Add the TfL Underground stations to the map
folium.GeoJson(data=gdf_underground_usage_s,
               marker=folium.CircleMarker(radius=4,  fill_color="royalblue", # this is the default marker and will be used
                                          fill_opacity=0.4, color="black", weight=1), # if we do not set the style_function
               style_function=lambda x: {'fillColor': 'royalblue', # color of the circle
                                          'color': 'royalblue', # color of the circle
                                          'weight': 0.7, # weight of the circle boundary
                                          'fillOpacity': 0.7, # transparency of the fill color
                                          'radius': x['properties']['usage'] /1000000 , # size of the circle according to the usage
                                          },
               tooltip=folium.GeoJsonTooltip(fields=['FULL_NAME', 'LINES', 'usage'], # tooltip text: station names and usage
                                             aliases=['Station Name:', 'Lines:', 'Annualised Usage:'], # tooltip text: station names and usage
                                             localize=True,
                                             sticky=True,
                                             )
               ).add_to(m)

m.save("fig/tfl_underground_stations2.html")
m
Make this Notebook Trusted to load map: File -> Trust Notebook

3.2.2 Polygon and Choropleth map#

We use the Choropleth function in Folium to plot the population density distribution in the UK LAs. The Choropleth can be customized with different colors, popups, and tooltips as well. We also use the folium.map.CustomPane to add the labels to the map. The folium.map.CustomPane is used to create a custom pane for the map, which allows us to add custom layers to the map.

gdf_las_pop = gdf_las_pop.to_crs("EPSG:4326") # convert the coordinate system to WGS84
# Create a map centered on London
m = folium.Map(location=[55.3781, -3.4360], # center of the UK
                tiles='CartoDB positron', # basemap
                zoom_start=6) # initial zoom level

# Add the UK LAs to the map
folium.Choropleth(geo_data=gdf_las_pop,
                  data=gdf_las_pop,
                  columns=['LAD24CD', 'density'], # columns to use for the choropleth
                  key_on='feature.properties.LAD24CD', # key to use for the choropleth
                  fill_color='RdPu', # color map
                  fill_opacity=1, # transparency of the fill color
                  line_opacity=0.8, # transparency of the line
                  line_color='black', # color of the line
                  line_weight=0.1, # weight of the line
                  legend_name='Population Density (per km^2)', # legend title
                  highlight=True, # highlight the polygon when hovering
                  use_jenks=True, # use the Jenks method to classify the data (see the change of the legend)
                  ).add_to(m)

# we can use custompane to show the label, here we use the CartoDB positron only labels
folium.map.CustomPane("labels").add_to(m)
# Add the labels to the map, this layer only shows the labels
folium.TileLayer("cartodbpositrononlylabels", pane="labels").add_to(m)
# m.save("fig/uk_pop_density_la.html")
# m
<folium.raster_layers.TileLayer at 0x3126f69e0>

As the folium.Choropleth() function does not support the tooltip inherently, we can use the folium.GeoJson() function to add the tooltip to the map. The folium.GeoJson() function can be used to add a GeoJSON layer to the map, and we can use the tooltip parameter to add the tooltip to the layer. Here, we need to define the colormaps in folium.

# Create a map centered on London
m = folium.Map(location=[55.3781, -3.4360], # center of the UK
                tiles='CartoDB positron', # basemap
                zoom_start=6) # initial zoom level

# Create a colormap
import branca.colormap as cm #
# create a colormap which is a linear colormap
linear = cm.linear.RdPu_09.scale(vmin=gdf_las_pop['density'].min(),
                           vmax=gdf_las_pop['density'].max()
                                 ).to_step(10) # step here is the number of colors in the colormap
# Add the UK LAs to the map
folium.GeoJson(data=gdf_las_pop,
                style_function=lambda x: {"fillColor": linear(x['properties']['density']), # color of the polygon
                "color": "black", # color of the polygon boundary
                "weight": 0.2, # weight of the polygon boundary
                "fillOpacity": 1, # transparency of the fill color
                 },
               tooltip=folium.GeoJsonTooltip(fields=['LAD24NM', 'density'], # tooltip text: station names and usage
                                             aliases=['LA Name:', 'Population Density:'], # tooltip text: station names and usage
                                             localize=True, # localize the text
                                             sticky=True, # sticky the text
                                             ),
               popup=folium.GeoJsonPopup(
                    fields=['population'], # popup text: station names and usage
                    aliases=['Population:'], # popup text: station names and usage
                    localize=True,
                    labels=True),
                    ).add_to(m)

# Add the colormap legend
linear.caption = 'Population Density (per km2)'
linear.add_to(m)

# we can use custompane to show the label, here we use the CartoDB positron only labels
folium.map.CustomPane("labels").add_to(m)
# Add the labels to the map, this layer only shows the labels
folium.TileLayer("cartodbpositrononlylabels", pane="labels").add_to(m)
m.save("fig/uk_pop_density_la_2.html")
# m

3.2.3 Polyline#

We can use the folium.PolyLine() function or folium.GeoJson() to visualize the linestring to the map. Note that the folium.PolyLine() function is used to create a polyline (with a list of coordinates) on the map, while the folium.GeoJson() function is used to add a GeoJSON layer to the map.

We use the London cycling route data to create a map of the cycling routes in London by using `folium.GeoJson().

# Read the London cycling route data
gdf_cycle_routes = gpd.read_file("data/Cycle_Routes.geojson")
# select the columns we need
gdf_cycle_routes = gdf_cycle_routes[['geometry', 'ROUTE_NAME', 'STATUS', 'ROUTE_LENGTH_KM']]
gdf_cycle_routes
geometry ROUTE_NAME STATUS ROUTE_LENGTH_KM
0 LINESTRING Z (-0.11025 51.529 0, -0.1103 51.52... Finsbury Park to Highbury Fields Feasibility 0.566
1 LINESTRING Z (-0.11475 51.46328 0, -0.11458 51... Brixton to Clapham High Street Complete 1.315
2 MULTILINESTRING Z ((-0.20319 51.51629 0, -0.20... Pembridge Square to Meanwhile Gardens Complete 2.296
3 MULTILINESTRING Z ((-0.16325 51.51393 0, -0.16... Hyde Park to Paddington Feasibility 0.757
4 LINESTRING Z (-0.12373 51.52611 0, -0.12331 51... Euston to Holborn Complete 1.181
... ... ... ... ...
161 MULTILINESTRING Z ((-0.11858 51.50975 0, -0.11... Blomsbury to Embankment Complete 1.421
162 LINESTRING Z (-0.15988 51.55558 0, -0.15913 51... Elephant and Castle to Hampstead Complete 1.833
163 LINESTRING Z (-0.33154 51.45099 0, -0.33132 51... Brentford to Twickenham Complete 4.291
164 LINESTRING Z (-0.04217 51.49002 0, -0.04295 51... London Bridge to Rotherhithe Roundabout In Progress 1.335
165 LINESTRING Z (-0.20691 51.4963 0, -0.20774 51.... Kensington High St to Shepherds Bush Complete 1.151

166 rows × 4 columns

# plot the cycling routes using folium.GeoJson()
# Create a map centered on London
m = folium.Map(location=[51.5074, -0.1278], # center of London
                tiles='CartoDB positron', # basemap
                zoom_start=13) # initial zoom level
# Add the cycling routes to the map
folium.GeoJson(data=gdf_cycle_routes,
               style_function=lambda x: {"color": "green", # color of the line
                                          "weight": 4 , # weight of the line according to the route length
                                          "opacity": 0.6, # transparency of the line
                                          },
               tooltip=folium.GeoJsonTooltip(fields=['ROUTE_NAME'], # tooltip text: route names
                                             aliases=['ROUTE_NAME:'],
                                             )
               ).add_to(m)
m.save("fig/london_cycle_route.html")
m
Make this Notebook Trusted to load map: File -> Trust Notebook

3.2.4 Map layer control and other plugins#

Using the folium.LayerControl() function, we can add the layer control to the map. The layer control allows us to toggle the visibility of different layers on the map. We can also use other plugins in Folium to add more functionality to the map, such as adding a search bar, adding a measure tool, and adding a fullscreen button. You can check the documentation for more details.

Now we use the london boundary data, TfL Underground stations, and Bus Stops to create an interactive map with the layer control and other plugins. we need three main functions to help use organise the map layers: folium.TileLayer() folium.FeatureGroup() and folium.LayerControl().

# london boundary and transform the coordinate system to WGS84
gdf_london_boundary = gdf_london_boundary.to_crs("EPSG:4326")
gdf_london_boundary
name geometry
0 Greater London POLYGON ((-0.16419 51.32604, -0.16308 51.33026...
# 1 Create a map centered on London without the basemap
m = folium.Map(location=[51.5074, -0.1278], # center of London
                zoom_start=14,
                tiles=None)

# 2 Add a basemap layer first and set a name for the layer (we can also add other basemaps)
folium.TileLayer('CartoDB positron', name='London transport stations').add_to(m)


# 3 Add the London boundary in a feature group, then add to the map
fg_boundary = folium.FeatureGroup(name='London Boundary') # 1. create a feature group for the London boundary
folium.GeoJson(data=gdf_london_boundary,
               style_function=lambda x: {"fillColor": "lightgrey", # color of the polygon
                                          "color": "black", # color of the polygon boundary
                                          "weight": 0.5, # weight of the polygon boundary
                                          "fillOpacity": 0.2, # transparency of the fill color
                                          },
                    ).add_to(fg_boundary) # 2. add the London boundary to the feature group
fg_boundary.add_to(m) # 3. add the feature group to the map

# 4 Add the TfL Underground stations in a feature group, then add to the map
fg_underground_stations = folium.FeatureGroup(name='TfL Underground Stations')
folium.GeoJson(data=gdf_underground_usage_s,
               marker=folium.Circle(radius=50, fill_color="Red", fill_opacity=0.8, color="black", weight=1),
               tooltip=folium.GeoJsonTooltip(fields=['FULL_NAME', 'LINES', 'usage'],
                                             aliases=['Station Name:', 'Lines:', 'Annualised Usage:'],
                                             localize=True,
                                             sticky=True,
                                             )
               ).add_to(fg_underground_stations)
fg_underground_stations.add_to(m)

# 5 Add the TfL Bus Stops in a feature group, then add to the map
fg_bus_stops = folium.FeatureGroup(name='TfL Bus Stops')
folium.GeoJson(data=gdf_bus_stop_s,
               marker=folium.Circle(radius=20, fill_color="royalblue", fill_opacity=0.7, color="black", weight=0.5),
               tooltip=folium.GeoJsonTooltip(fields=['Stop_Name'],
                                             aliases=['Bus Stop Name:'],
                                             localize=True,
                                             sticky=True,
                                             )
               ).add_to(fg_bus_stops)
fg_bus_stops.add_to(m)

# 6 Add the Fullscreen plugins
from folium import plugins
plugins.Fullscreen(
    position="topright",
    title="Expand me",
    title_cancel="Exit me",
    force_separate_button=True,
).add_to(m)

# 7 Add the Measure plugins
plugins.Draw(export=True).add_to(m)
# add the geocoder search bar
plugins.Geocoder().add_to(m)
# 7 Add the layer control to the map
folium.LayerControl(collapsed=False).add_to(m)
m.save("fig/london_transport_stations_bus_stop.html")
# m

Quiz#

  • Q1: Use London cycling route data (data/Cycle_Routes.geojson) or you can use the gdf_cycle_routes to create a static map of the cycling routes in London. In the map, the color of the cycling routes should be plotted according to the status of the route (use the [‘STATUS’] column). And this map should have a London boundary, north arrow, legend, and scale bar.

  • Q2: Use the London floo-risk data (you can find the data under the data/flood_risk_London folder) to explore the geodataframe:

    • What is the coordinate system of the data?

    • What is the column name for the flooding risk level?

    • How many unique flooding risk levels are there?

    • Calculate the area of the flooding risk polygons and print the total areas of different flooding risk levels.

    • Create a static map of the flooding risk areas in London and use different colors to represent different flooding risk levels.

  • Q3: Use the London floo-risk data to create an interactive map, the flooding areas should be colored by the flooding risk level. You need to set the style_function parameters. Add a geocode search bar to the map and search for London Eye to determine the flood risk level of its location.

Q1 solution:

gdf_cycle_routes
geometry ROUTE_NAME STATUS ROUTE_LENGTH_KM
0 LINESTRING Z (-0.11025 51.529 0, -0.1103 51.52... Finsbury Park to Highbury Fields Feasibility 0.566
1 LINESTRING Z (-0.11475 51.46328 0, -0.11458 51... Brixton to Clapham High Street Complete 1.315
2 MULTILINESTRING Z ((-0.20319 51.51629 0, -0.20... Pembridge Square to Meanwhile Gardens Complete 2.296
3 MULTILINESTRING Z ((-0.16325 51.51393 0, -0.16... Hyde Park to Paddington Feasibility 0.757
4 LINESTRING Z (-0.12373 51.52611 0, -0.12331 51... Euston to Holborn Complete 1.181
... ... ... ... ...
161 MULTILINESTRING Z ((-0.11858 51.50975 0, -0.11... Blomsbury to Embankment Complete 1.421
162 LINESTRING Z (-0.15988 51.55558 0, -0.15913 51... Elephant and Castle to Hampstead Complete 1.833
163 LINESTRING Z (-0.33154 51.45099 0, -0.33132 51... Brentford to Twickenham Complete 4.291
164 LINESTRING Z (-0.04217 51.49002 0, -0.04295 51... London Bridge to Rotherhithe Roundabout In Progress 1.335
165 LINESTRING Z (-0.20691 51.4963 0, -0.20774 51.... Kensington High St to Shepherds Bush Complete 1.151

166 rows × 4 columns

# You can use the gdf_cycle_route we created in the previous section, and we use geopandas to plot the static map.
# Create a figure and axis
fig, ax = plt.subplots(figsize=(8, 8))
# Plot the London boundary
gdf_london_boundary.plot(ax=ax, # ax is the axis to plot on
                          color='lightgrey', # color of the polygon
                          edgecolor='black', # color of the boundary
                          alpha=0.1, # transparency of all colors
                          linewidth=0.5) # linewidth of the polygon boundary
# The cycling routes plot
gdf_cycle_routes.plot(ax=ax,
                      column='STATUS', # column name to tailor the dot map, here is the categorical data
                      cmap='tab10', # color map
                      alpha=0.8,
                      legend = True,
                      legend_kwds = {"loc" : "lower right", # legend position
                                     "fontsize": 8, # legend label font size
                                     "title_fontsize": 10, # legend title font size
                                     "title": "Cycling Routes Status", # legend title
                                    },
                      )

# Add the North Arrow and scale bar to the map
north_arrow(ax, location="upper right", rotation={"crs": gdf_cycle_routes.crs, "reference": "center"})
scale_bar(ax, location="lower left", style="boxes", bar={"projection": gdf_cycle_routes.crs})
# Set the title
ax.set_title('Cycling Route Status in London')
ax.set_xlabel('Easting (m)')
ax.set_ylabel('Northing (m)')
plt.show()
_images/c55bc4778f0c28b94fcee0683d9d72e7f81039f818eb4d9b3dbfaaad205ce5be.png

Q2 solution:

# Read the London flood risk data
gdf_flood_risk = gpd.read_file("data/flood_risk_London/RoFRS_London.shp")
gdf_flood_risk.crs
<Projected CRS: EPSG:27700>
Name: OSGB36 / British National Grid
Axis Info [cartesian]:
- E[east]: Easting (metre)
- N[north]: Northing (metre)
Area of Use:
- name: United Kingdom (UK) - offshore to boundary of UKCS within 49°45'N to 61°N and 9°W to 2°E; onshore Great Britain (England, Wales and Scotland). Isle of Man onshore.
- bounds: (-9.01, 49.75, 2.01, 61.01)
Coordinate Operation:
- name: British National Grid
- method: Transverse Mercator
Datum: Ordnance Survey of Great Britain 1936
- Ellipsoid: Airy 1830
- Prime Meridian: Greenwich
# we found the risk level is in the column 'PROB_4BAND'
gdf_flood_risk
OBJECTID PROB_4BAND SUITABILIT PUB_DATE SHAPE_Leng SHAPE_Area geometry
0 51657 High County to Town 20180328 154.684691 223.985278 POLYGON ((533783.154 158782.957, 533783.149 15...
1 51662 High County to Town 20180328 27.586314 23.741519 POLYGON ((533777.986 158805, 533775 158805, 53...
2 51890 High County to Town 20180328 1304.269103 3885.963947 POLYGON ((533173.59 159630, 533170.35 159622.0...
3 51936 High County to Town 20180328 382.747355 701.640792 POLYGON ((533155 159663.871, 533155 159660, 53...
4 51937 High County to Town 20180328 13.698716 7.911297 POLYGON ((533015 159776.44, 533010.556 159780,...
... ... ... ... ... ... ... ...
17218 1270196 Very Low Town to Street 20180328 3718.106019 134032.404292 POLYGON ((537600 199185.854, 537600 199150, 53...
17219 1270200 Very Low Town to Street 20180328 2558.957375 56558.336629 POLYGON ((537650 199434.633, 537650 199400, 53...
17220 1270201 Very Low Town to Street 20180328 99.845313 124.410861 POLYGON ((537550 199558.086, 537546.96 199561....
17221 1270202 Very Low Town to Street 20180328 1259.917295 36923.341337 POLYGON ((537450 199119.375, 537450 199150, 53...
17222 1270204 Very Low Town to Street 20180328 383.290683 6149.534341 POLYGON ((537600 199754.275, 537600 199750, 53...

17223 rows × 7 columns

# risk area/polygon unique numbers
print('risk area/polygon numbers', gdf_flood_risk.geometry.nunique())
risk area/polygon numbers 17223
gdf_flood_risk.PROB_4BAND.unique()
array(['High', 'Low', 'Medium', 'Very Low'], dtype=object)
# calculate the area of the flood risk polygons
gdf_flood_risk['polygon_area'] = gdf_flood_risk.geometry.area #m2
gdf_flood_risk
OBJECTID PROB_4BAND SUITABILIT PUB_DATE SHAPE_Leng SHAPE_Area geometry polygon_area
0 51657 High County to Town 20180328 154.684691 223.985278 POLYGON ((533783.154 158782.957, 533783.149 15... 12.142567
1 51662 High County to Town 20180328 27.586314 23.741519 POLYGON ((533777.986 158805, 533775 158805, 53... 9.721701
2 51890 High County to Town 20180328 1304.269103 3885.963947 POLYGON ((533173.59 159630, 533170.35 159622.0... 3855.618494
3 51936 High County to Town 20180328 382.747355 701.640792 POLYGON ((533155 159663.871, 533155 159660, 53... 701.640792
4 51937 High County to Town 20180328 13.698716 7.911297 POLYGON ((533015 159776.44, 533010.556 159780,... 7.911297
... ... ... ... ... ... ... ... ...
17218 1270196 Very Low Town to Street 20180328 3718.106019 134032.404292 POLYGON ((537600 199185.854, 537600 199150, 53... 134032.404292
17219 1270200 Very Low Town to Street 20180328 2558.957375 56558.336629 POLYGON ((537650 199434.633, 537650 199400, 53... 56558.336629
17220 1270201 Very Low Town to Street 20180328 99.845313 124.410861 POLYGON ((537550 199558.086, 537546.96 199561.... 124.410861
17221 1270202 Very Low Town to Street 20180328 1259.917295 36923.341337 POLYGON ((537450 199119.375, 537450 199150, 53... 36923.341337
17222 1270204 Very Low Town to Street 20180328 383.290683 6149.534341 POLYGON ((537600 199754.275, 537600 199750, 53... 6149.534341

17223 rows × 8 columns

# print the total areas of different risk levels
gdf_flood_risk[['PROB_4BAND', 'polygon_area']].groupby('PROB_4BAND').sum()['polygon_area'] / 1e6  # km2
PROB_4BAND
High        49.446792
Low         44.774430
Medium      35.108733
Very Low    95.499641
Name: polygon_area, dtype: float64
gdf_flood_risk
OBJECTID PROB_4BAND SUITABILIT PUB_DATE SHAPE_Leng SHAPE_Area geometry polygon_area
0 51657 High County to Town 20180328 154.684691 223.985278 POLYGON ((533783.154 158782.957, 533783.149 15... 12.142567
1 51662 High County to Town 20180328 27.586314 23.741519 POLYGON ((533777.986 158805, 533775 158805, 53... 9.721701
2 51890 High County to Town 20180328 1304.269103 3885.963947 POLYGON ((533173.59 159630, 533170.35 159622.0... 3855.618494
3 51936 High County to Town 20180328 382.747355 701.640792 POLYGON ((533155 159663.871, 533155 159660, 53... 701.640792
4 51937 High County to Town 20180328 13.698716 7.911297 POLYGON ((533015 159776.44, 533010.556 159780,... 7.911297
... ... ... ... ... ... ... ... ...
17218 1270196 Very Low Town to Street 20180328 3718.106019 134032.404292 POLYGON ((537600 199185.854, 537600 199150, 53... 134032.404292
17219 1270200 Very Low Town to Street 20180328 2558.957375 56558.336629 POLYGON ((537650 199434.633, 537650 199400, 53... 56558.336629
17220 1270201 Very Low Town to Street 20180328 99.845313 124.410861 POLYGON ((537550 199558.086, 537546.96 199561.... 124.410861
17221 1270202 Very Low Town to Street 20180328 1259.917295 36923.341337 POLYGON ((537450 199119.375, 537450 199150, 53... 36923.341337
17222 1270204 Very Low Town to Street 20180328 383.290683 6149.534341 POLYGON ((537600 199754.275, 537600 199750, 53... 6149.534341

17223 rows × 8 columns

# Plot the flood risk areas using geopandas
# Create a figure and axis
import matplotlib.patches as mpatches
fig, ax = plt.subplots(figsize=(8, 8))
# plot the flood risk levels with different colors, the colormap can distinguish the risk levels, we can use lopp to plot the different risk level polygon
# Define the color map manually
risk_colors = {
    'High': 'red',
    'Medium': 'orange',
    'Low': 'yellow',
    'Very Low': 'royalblue'
}

# Plot each risk level
for level, color in risk_colors.items():
    gdf_flood_risk[gdf_flood_risk['PROB_4BAND'] == level].plot(ax=ax,
                                                                color=color,
                                                                edgecolor='black',
                                                                alpha=0.8,
                                                                linewidth=0.01)

# Create custom legend handles
legend_handles = [mpatches.Patch(color=color, label=level) for level, color in risk_colors.items()]
ax.legend(handles=legend_handles, loc='lower right', fontsize=10, title='Flood Risk', title_fontsize=12, frameon=False)
# Add the North Arrow and scale bar to the map
north_arrow(ax, location="upper right", rotation={"crs": gdf_flood_risk.crs, "reference": "center"})
scale_bar(ax, location="lower left", style="boxes", bar={"projection": gdf_flood_risk.crs})
# Set the title
ax.set_title('Flood Risk Areas in London')
ax.set_xlabel('Easting (m)')
ax.set_ylabel('Northing (m)')
plt.show()
_images/6ab29e28f69d5d639ebec9a18a32782ad1b1deac05c420e714728a151364c220.png

Q3 solution:

gdf_flood_risk = gdf_flood_risk.to_crs("EPSG:4326") # convert the coordinate system to WGS84
# Create a map centered on London
m = folium.Map(location=[51.5074, -0.1278], # center of London
                tiles='OpenStreetMap', # basemap
                zoom_start=12) # initial zoom level
# Add the London flood risk data to the map, and we plot the color of the polygon according to the flood risk levels
folium.GeoJson(data=gdf_flood_risk,
               style_function=lambda x: {"fillColor": "red" if x['properties']['PROB_4BAND'] == 'High' else "orange" if x['properties']['PROB_4BAND'] == 'Medium' else "yellow" if x['properties']['PROB_4BAND'] == 'Low' else "royalblue", # color of the polygon
                                          "color": "black", # color of the polygon boundary
                                          "weight": 0.1, # weight of the polygon boundary
                                          "fillOpacity": 0.5, # transparency of the fill color
                                          },
               tooltip=folium.GeoJsonTooltip(fields=['PROB_4BAND'], # tooltip text: flood risk levels
                                             aliases=['Flood Risk Level:'],
                                             )
               ).add_to(m)
# Add a geocode search bar to the map.
plugins.Geocoder().add_to(m)
m.save("fig/london_flood_risk.html")
# m