Introduction¶
The maps aim to assist individuals new to Leicester, including those arriving from other parts of the UK or abroad, whether considering a move or planning a visit. This map will serve as a visual tool, presenting essential information about different areas within Leicester. Newcomers to Leicester who wants to move into the city, dont have an easy access to find information different of what the areas is like. The aim to unveil the various areas of Leicester through an interactive map that that provide a summary of the demographic, economic, and availability of local public facilities to the users.
Retrieving the data¶
The two main areas which I pull the data are from:
https://data.leicester.gov.uk/pages/home/. Open Leicester contains information about the Leicester population, census data, public facilities, and more. The majority of the data used are from Open Leicester.
Also, the following link: https://docs.developer.yelp.com. Yelp API, specifically, I will be using Yelp Fusion API, which contains local business reviews and information in a specific area. This data will add more information to the map and give the user an idea of places to dine/eat within the area.
import requests
import json
import pandas as pd
import folium
from folium import plugins
from geopy.geocoders import Nominatim
from geopy.extra.rate_limiter import RateLimiter
import numpy as np
import branca.colormap as cm
Sourcing the data - Population Density Data¶
Sourcing the data from - https://data.leicester.gov.uk/pages/home/
The API used are from Leicester City Council open data which are free to use. The API works by sending a 'GET' request to the endpoint, the endpoint in this case is the URL (https://data.leicester.gov.uk/api/explore/v2.1/catalog/datasets/census-21-ward-population-density/records?limit=22) to retreive population density of each ward in Leicester. The endpoint provides access to the data.
The server then process the response and the inquiry, the repsonse includes the status code which shows if the response was successful or not. The value 200 indicates that the response was successful and 404 indicates that the response was not successful.
The response provides the required dataset as a json data which then is transformed into pandas dataframe to be use for data analysis.
# Population data of leicester, England. using API
response = requests.get("https://data.leicester.gov.uk/api/explore/v2.1/catalog/datasets/census-21-ward-population-density/records?limit=22")
# print the status code
print(response.status_code)
# view the data in panda dataframe
population_df = pd.DataFrame(response.json()['results'])
population_df.head()
200
electoral_wards_and_divisions_code | electoral_wards_and_divisions | observation | leicester_value | proportional_value | geo_shape | geo_point_2d | |
---|---|---|---|---|---|---|---|
0 | E05010459 | Aylestone | 3133 | 5027 | 0.623369 | {'type': 'Feature', 'geometry': {'coordinates'... | {'lon': -1.1545145546789126, 'lat': 52.6027793... |
1 | E05010464 | Evington | 3420 | 5027 | 0.680299 | {'type': 'Feature', 'geometry': {'coordinates'... | {'lon': -1.0758455482423877, 'lat': 52.6271693... |
2 | E05010465 | Eyres Monsell | 5176 | 5027 | 1.029659 | {'type': 'Feature', 'geometry': {'coordinates'... | {'lon': -1.145109294352698, 'lat': 52.59065596... |
3 | E05010471 | Saffron | 5013 | 5027 | 0.997175 | {'type': 'Feature', 'geometry': {'coordinates'... | {'lon': -1.1367771857850641, 'lat': 52.6124368... |
4 | E05010474 | Thurncourt | 5703 | 5027 | 1.134608 | {'type': 'Feature', 'geometry': {'coordinates'... | {'lon': -1.0625848218854592, 'lat': 52.6389170... |
Data cleaning/exploration - Population Density Data¶
Normalising the geo_shape and geo_point_2d to visualise the data structure and cleaning up the data by removing columns that are not required along with renaming the columns to a suitable name.
# Flattening the column Geo_point_2d
population_df['longitude'] = population_df['geo_point_2d'].apply(lambda x: x['lon'])
population_df['latitude'] = population_df['geo_point_2d'].apply(lambda x: x['lat'])
# Flattening the column geo_shape
population_df['geo_shape_type'] = population_df['geo_shape'].apply(lambda x: x['type'])
population_df['geo_shape_coordinates'] = population_df['geo_shape'].apply(lambda x: x['geometry']['coordinates'])
population_df.columns
Index(['electoral_wards_and_divisions_code', 'electoral_wards_and_divisions', 'observation', 'leicester_value', 'proportional_value', 'geo_shape', 'geo_point_2d', 'longitude', 'latitude', 'geo_shape_type', 'geo_shape_coordinates'], dtype='object')
# removing columns that are required
population_df = population_df[['electoral_wards_and_divisions_code', 'observation', 'electoral_wards_and_divisions']]
# renaming columns
population_df.columns = ['WardCode', 'PopulationDensity', 'ward_name']
population_df.head()
WardCode | PopulationDensity | ward_name | |
---|---|---|---|
0 | E05010459 | 3133 | Aylestone |
1 | E05010464 | 3420 | Evington |
2 | E05010465 | 5176 | Eyres Monsell |
3 | E05010471 | 5013 | Saffron |
4 | E05010474 | 5703 | Thurncourt |
Sourcing the Data and cleaning/exploration the data - Unemployment Data¶
Retrieving the unemployment data from Open Leicester and cleaning the data.
# Data to show the amount of people claiming for umepmployment in the ward
response2 = requests.get("https://data.leicester.gov.uk/api/explore/v2.1/catalog/datasets/claimant-count-ward-map-data/records?limit=22")
# print the status code and put the data into pandas dataframe.
print(response2.status_code)
unemployment_rate = pd.DataFrame(response2.json())
200
# Normalize the data including the nested 'geo_point_2d' and then print
normalized_df = pd.json_normalize(unemployment_rate['results'], sep='_')
normalized_df.head()
date | geography_name | geography_code | obs_value | claimant_rate_of_16_64_population | total | geo_point_2d_lon | geo_point_2d_lat | |
---|---|---|---|---|---|---|---|---|
0 | 2025-03-01 | Aylestone | E05010459 | 380 | 5.582489 | 6807 | -1.154515 | 52.602779 |
1 | 2025-03-01 | Beaumont Leys | E05010460 | 785 | 7.180754 | 10932 | -1.162397 | 52.669255 |
2 | 2025-03-01 | Belgrave | E05010461 | 740 | 6.552152 | 11294 | -1.119714 | 52.650198 |
3 | 2025-03-01 | Braunstone Park & Rowley Fields | E05010462 | 980 | 7.752551 | 12641 | -1.171117 | 52.622438 |
4 | 2025-03-01 | North Evington | E05010469 | 1135 | 8.264764 | 13733 | -1.099745 | 52.640469 |
# removing columns that are not relevant and print them out
unemployment_rate_df = normalized_df[['geography_code', 'obs_value']]
unemployment_rate_df.columns = ['WardCode', 'UnemploymentValue']
unemployment_rate_df.head()
WardCode | UnemploymentValue | |
---|---|---|
0 | E05010459 | 380 |
1 | E05010460 | 785 |
2 | E05010461 | 740 |
3 | E05010462 | 980 |
4 | E05010469 | 1135 |
Sourcing the Data and cleaning/exploration the data - population by age and sex Data¶
Retrieving the population by are and sex data from Open Leicester and cleaning the data. The dataset did not contain the mean age of the population by ward, therefore, a code is required to calculate the average age.
# Leicester data on population by age and sex by ward
age_df = pd.read_json('census-21-pop-by-age-and-sex-ward.json')
age_df.head()
electoral_wards_and_divisions_code | electoral_wards_and_divisions | age | age_group | sex | observation | |
---|---|---|---|---|---|---|
0 | E05010458 | Abbey | 2 | 00 to 04 | Male | 182 |
1 | E05010458 | Abbey | 4 | 00 to 04 | Male | 168 |
2 | E05010458 | Abbey | 6 | 05 to 09 | Male | 165 |
3 | E05010458 | Abbey | 7 | 05 to 09 | Female | 158 |
4 | E05010458 | Abbey | 7 | 05 to 09 | Male | 184 |
Calculating the average age.
# group the town columns so that it can calculate the average value
grouped = age_df.groupby(['electoral_wards_and_divisions_code', 'electoral_wards_and_divisions'])
# Calculate the weighted average age for each group
average_age_per_ward = grouped.apply(lambda x: (x['age'] * x['observation']).sum() / x['observation'].sum()).reset_index(name='Average Age')
Removing the unnecessary columns.
# make a copy so it doesnt effect the orignal data
average_age_df = average_age_per_ward[['electoral_wards_and_divisions_code', 'Average Age']].copy()
# rename the column
average_age_df.columns = ['WardCode', 'AverageAge']
# print the dataframe
print(average_age_df.head())
average_age_df.columns
WardCode AverageAge 0 E05010458 35.639879 1 E05010459 39.122425 2 E05010460 35.869893 3 E05010461 38.587706 4 E05010462 35.509348
Index(['WardCode', 'AverageAge'], dtype='object')
Interactive Map - 1¶
The immediate idea is to plot an interactive map to visualize the population by each ward, where the user can see different information when they hover over it. The colour #ff0000 (red) is used when the mouse hovers over the map; it distinguishes between the neighbourhood wards. Setting the style colour to light yellow (#ffeda0) with black borders makes it easy for the eyes to focus on a specific area.
# Load the GeoJSON data of the ward
geojson_path = 'census-21-ward-population-density.geojson'
with open(geojson_path, 'r') as file:
geojson_data = json.load(file)
# create a dictionary that contains population_df dataframe with WardCode and PopulationDensity
population_dict = population_df.set_index('WardCode')['PopulationDensity'].to_dict()
# Loop through the geojson data to update the value in the dictionary
for feature in geojson_data['features']:
ward_code = feature['properties']['electoral_wards_and_divisions_code']
if ward_code in population_dict:
feature['properties']['PopulationDensity'] = population_dict[ward_code]
# start the map with leicester location
map = folium.Map(location=[52.6369, -1.1398], zoom_start=12, min_zoom=11)
# Function to style the geojson features
def style_function(feature):
return {
'fillColor': '#ffeda0',
'color': 'black',
'weight': 0.5,
'fillOpacity': 0.7
}
# Function to hightlight the feature when its been hovered
def highlight_function(feature):
return {
'fillColor': '#ff0000',
'color': 'black',
'weight': 0.5,
'fillOpacity': 1.0
}
# create the interactive layer using the geojson data
interactive_layer = folium.GeoJson(
geojson_data,
style_function=style_function,
highlight_function=highlight_function,
tooltip=folium.GeoJsonTooltip(
fields=['electoral_wards_and_divisions', 'PopulationDensity'],
aliases=['Ward:', 'Density:'],
localize=True
),
popup=folium.GeoJsonPopup(fields=['electoral_wards_and_divisions', 'PopulationDensity'])
).add_to(map)
map.save('map1.html')
map
Interactive Map - 2¶
Three choropleth layers are added to represent the different datasets (population density, unemployment rate, and average age) using folium choropleth. I have chosen to use orange for population, blue for unemployment and green for age to distinguish between the data. The map visually represents the data across different areas, using shades or colours to indicate the density levels. Various shades of colours, ranging from light to dark, make the map easier to understand. The darker shades represent areas with higher population density. The map can toggle between different layers of choropleth maps to visualise various datasets.
# Start the map
map = folium.Map(location=[52.6369, -1.1398], zoom_start=12, min_zoom=10)
# Add the choropleth layers
population = folium.Choropleth(
geo_data=geojson_data,
name='Population Density',
data=population_df,
columns=['WardCode', 'PopulationDensity'],
key_on='feature.properties.electoral_wards_and_divisions_code',
fill_color='Oranges',
fill_opacity=0.7,
line_opacity=0.2,
legend_name='Population Density',
overlay=False,
show=True
).add_to(map)
unemployment = folium.Choropleth(
geo_data=geojson_data,
name='Unemployment Rate',
data=unemployment_rate_df,
columns=['WardCode', 'UnemploymentValue'],
key_on='feature.properties.electoral_wards_and_divisions_code',
fill_color='Blues',
fill_opacity=0.7,
line_opacity=0.2,
legend_name='Unemployment Rate',
overlay=False,
show=False
).add_to(map)
average_age = folium.Choropleth(
geo_data=geojson_data,
name='Average Age',
data=average_age_df,
columns=['WardCode', 'AverageAge'],
key_on='feature.properties.electoral_wards_and_divisions_code',
fill_color='YlGn',
fill_opacity=0.7,
line_opacity=0.2,
legend_name='Average Age',
overlay=False,
show=False
).add_to(map)
# Add the base map tile layer with overlay=True and a name for toggling
folium.TileLayer('OpenStreetMap', overlay=True, name="Map tiles").add_to(map)
# Add layer controls to toggle layers on and off
folium.LayerControl().add_to(map)
map.save("map2.html")
map
Interactive Map - 3¶
By combining the ideas of maps 1 and 2, the map can be toggled to view each choropleth information and hover over each ward to see the popup information. It presents the demographic information of Leicester's population density, average age, and unemployment rate of each ward.
# Start the map
map = folium.Map(location=[52.6369, -1.1398], zoom_start=12, min_zoom=11)
# Find min and max values for each dataset to scale the color maps accurately
population_min, population_max = population_df['PopulationDensity'].min(), population_df['PopulationDensity'].max()
age_min, age_max = average_age_df['AverageAge'].min(), average_age_df['AverageAge'].max()
unemployment_min, unemployment_max = unemployment_rate_df['UnemploymentValue'].min(), unemployment_rate_df['UnemploymentValue'].max()
# Define color maps for each data type
population_colormap = cm.linear.YlOrRd_09.scale(population_min, population_max)
age_colormap = cm.linear.YlGn_09.scale(age_min, age_max)
unemployment_colormap = cm.linear.Blues_09.scale(unemployment_min, unemployment_max)
# Function to update GeoJSON properties with DataFrame values
def update_geojson_properties(geojson, df, property_name, df_value_column, geojson_key='electoral_wards_and_divisions_code', df_key='WardCode'):
for feature in geojson['features']:
ward_code = feature['properties'][geojson_key]
match = df[df[df_key] == ward_code]
if not match.empty:
# Convert the value from numpy data type to python data type
value = match[df_value_column].values[0]
if isinstance(value, np.number):
value = value.item()
feature['properties'][property_name] = value
# Update GeoJSON with 'AverageAge', 'PopulationDensity', and 'UnemploymentValue'
update_geojson_properties(geojson_data, average_age_df, 'AverageAge', 'AverageAge')
update_geojson_properties(geojson_data, population_df, 'PopulationDensity', 'PopulationDensity')
update_geojson_properties(geojson_data, unemployment_rate_df, 'UnemploymentValue', 'UnemploymentValue')
# Function to create a GeoJson layer
def create_layer(data, colormap, field_name, layer_name):
def style_function(feature):
value = feature['properties'].get(field_name, 0)
return {
'fillColor': colormap(value) if value else 'transparent',
'color': 'black',
'weight': 1,
'fillOpacity': 0.5
}
def highlight_function(feature):
return {
'fillColor': '#ff0000',
'color': 'black',
'weight': 2,
'fillOpacity': 0.7
}
tooltip = folium.GeoJsonTooltip(
fields=['electoral_wards_and_divisions', field_name],
aliases=['Ward:', layer_name + ':'],
localize=True
)
layer = folium.GeoJson(
data,
style_function=style_function,
highlight_function=highlight_function,
tooltip=tooltip,
name=layer_name
)
colormap.add_to(map)
layer.add_to(map)
return layer
# Create and add layers to map
population_layer = create_layer(geojson_data, population_colormap, 'PopulationDensity', 'Population Density')
age_layer = create_layer(geojson_data, age_colormap, 'AverageAge', 'Average Age')
unemployment_layer = create_layer(geojson_data, unemployment_colormap, 'UnemploymentValue', 'Unemployment Rate')
folium.LayerControl().add_to(map)
map.save('map3.html')
map
Sourcing the data - Yelp API¶
I used the Yelp API to retrieve information on the restaurant business in Leicester, England. However, retrieving all the restaurant information using the Yelp API was only possible if I paid to access the full service. Therefore, I was limited to retrieving a maximum of 50 business information. I used the following link, https://docs.developer.yelp.com/docs/fusion-intro, to understand the documentation and the format needed to pull the information I needed.
By registering with Yelp, I was provided with a client ID and the API key to authenticate and make a request to retrieve information about restaurants in the Leicester area. I have set the parameters to Leicester, restaurants limited to 50, and a radius of 10000km from Leicester.
The API response is a JSON object with business information about the restaurant, such as its name, address, rating, and phone number. An empty dictionary is created to loop through the data and store the result, sorted by category.
After retrieving the data from Yelp, I normalised it and converted the list of business information into a pandas dataframe. Geocoding was required to retrieve the business locations (longitude and latitude), which can then be used to place markers on the map.
# Yelp API to get restaurants data in Leicester. Maximum is 50 requests
client_ID = ''
API_key = ''
headers = {
'Authorization': f'Bearer {API_key}',
}
url = 'https://api.yelp.com/v3/businesses/search'
params = {
'location': 'Leicester',
'term': 'restaurants',
'limit': 50,
'radius': 10000
}
response = requests.get(url, headers=headers, params=params)
# create an empty dictionary to store the data
categories_dict = {}
businesses = response.json().get('businesses', [])
# Iterate through each business
for business in businesses:
# Extract categories
business_categories = business.get('categories', [])
for category in business_categories:
category_title = category['title']
# Prepare business data
business_data = {
'name': business['name'],
'rating': business.get('rating', 'No rating'),
'address': " ".join(business['location'].get('display_address', 'No address')),
'phone': business.get('phone', 'No phone number'),
}
# If the category is already in the dictionary, add the business data to the list
if category_title in categories_dict:
categories_dict[category_title].append(business_data)
else:
# Otherwise, create a new list with this category
categories_dict[category_title] = [business_data]
print(categories_dict)
{'Indian': [{'name': 'Kayal', 'rating': 4.7, 'address': '153 Granby street Leicester LE1 6FE United Kingdom', 'phone': '+441162554667'}, {'name': 'Shivalli', 'rating': 4.4, 'address': '21 Welford Road Leicester LE2 7AD United Kingdom', 'phone': '+441162550137'}, {'name': 'The Curry House', 'rating': 4.5, 'address': '118 London Road Leicester LE2 0QS United Kingdom', 'phone': '+441162550688'}, {'name': 'Feast India', 'rating': 3.8, 'address': '411 Melton Road Leicester LE4 7PA United Kingdom', 'phone': '+441162582590'}, {'name': "Paddy's Marten Inn", 'rating': 5.0, 'address': '98 Martin Street Leicester LE4 6EU United Kingdom', 'phone': '+441162665123'}, {'name': 'Bombay Street Kitchen', 'rating': 5.0, 'address': '29 Melton Road Leicester LE4 6PN United Kingdom', 'phone': '+441162681142'}, {'name': 'Mem-Saab', 'rating': 4.0, 'address': '59-59a Highcross Street Leicester Leicester LE1 4PG United Kingdom', 'phone': '+441162530243'}, {'name': 'Bombay Bites', 'rating': 4.0, 'address': '41A Belvoir Street Leicester LE1 6SL United Kingdom', 'phone': '+441162852299'}, {'name': 'Flamingo Bar & Grill', 'rating': 5.0, 'address': '179-183 Loughborough Road Leicester LE4 5LR United Kingdom', 'phone': '+441162610109'}], 'British': [{'name': 'Mrs. Bridges Tea Rooms', 'rating': 4.7, 'address': '17 Loseby Lane Leicester LE1 5DR United Kingdom', 'phone': '+441162623131'}, {'name': 'Cosy Club', 'rating': 5.0, 'address': '62-68 Highcross Street Leicester LE1 4NN United Kingdom', 'phone': '+441164080008'}, {'name': 'The Case', 'rating': 3.8, 'address': '4-6 Hotel Street Leicester LE1 5AW United Kingdom', 'phone': '+441162517675'}, {'name': "Bill's", 'rating': 4.0, 'address': '10 Shires Lane Highcross Leicester LE1 4AN United Kingdom', 'phone': '+441162511258'}, {'name': 'Black Iron Restaurant', 'rating': 4.0, 'address': 'Hinckley Road Leicester LE3 1HX United Kingdom', 'phone': '+441163665642'}, {'name': 'Maiyango', 'rating': 4.1, 'address': 'Unit 13-21 St Nicholas Place Leicester LE1 4LD United Kingdom', 'phone': '+441162518898'}, {'name': 'Grey Lady', 'rating': 5.0, 'address': 'Sharpley Hill Newtown Linford Leicester LE6 0AH United Kingdom', 'phone': '+441530243558'}, {'name': 'The Forge', 'rating': 4.0, 'address': 'Main Street Glenfield Leicester LE3 8DG United Kingdom', 'phone': '+441162871702'}], 'French': [{'name': 'Le Bistrot Pierre', 'rating': 4.0, 'address': '8-10 Millstone Lane Leicester LE1 5JN United Kingdom', 'phone': '+441162627927'}], 'Bars': [{'name': 'Cosy Club', 'rating': 5.0, 'address': '62-68 Highcross Street Leicester LE1 4NN United Kingdom', 'phone': '+441164080008'}], 'Cafes': [{'name': 'Cosy Club', 'rating': 5.0, 'address': '62-68 Highcross Street Leicester LE1 4NN United Kingdom', 'phone': '+441164080008'}, {'name': 'Prana Cafe', 'rating': 5.0, 'address': '10 Horsefair street Leicester LE1 5BN United Kingdom', 'phone': '+441162547569'}, {'name': 'Entropy', 'rating': 4.7, 'address': '42 Hinckley Road Leicester Forest East LE3 3 United Kingdom', 'phone': '+441162259650'}], 'Chinese': [{'name': 'Terracotta Restaurant', 'rating': 3.6, 'address': '25-27 Highcross Street Leicester LE1 4PF United Kingdom', 'phone': '+441162536666'}, {'name': 'The Real China', 'rating': 3.8, 'address': 'Unit R10 14 High Cross Leicester LE1 4SD United Kingdom', 'phone': '+441162628288'}, {'name': 'Shanghai Moon', 'rating': 3.5, 'address': '76-78 High Street Leicester LE1 5YP United Kingdom', 'phone': '+441162624937'}, {'name': 'Sichuan Brothers Restaurant', 'rating': 4.5, 'address': '169A London Road Leicester LE2 1EG United Kingdom', 'phone': '+441162547302'}, {'name': 'Red Lantern', 'rating': 4.7, 'address': '16 Highfield St Leicester LE2 1AB United Kingdom', 'phone': '+441162852006'}, {'name': 'Royal Chef', 'rating': 4.8, 'address': '202-204 Narborough Rd Leicester LE3 0DL United Kingdom', 'phone': '+441162244228'}, {'name': 'Big Wang', 'rating': 4.0, 'address': '219 Welford Road Leicester LE2 6BH United Kingdom', 'phone': '+441162708080'}], 'Italian': [{'name': 'Casa Romana', 'rating': 4.7, 'address': '5 Albion Street Leicester LE1 6GD United Kingdom', 'phone': '+441162541174'}, {'name': 'San Carlo', 'rating': 3.5, 'address': 'Granby Street Leicester LE1 1DE United Kingdom', 'phone': '+441162519332'}, {'name': 'Bru', 'rating': 4.7, 'address': '20-22 Granby Street City Centre LE1 1DE United Kingdom', 'phone': '+441162519667'}, {'name': 'Prezzo', 'rating': 4.8, 'address': '22 Silver Street Leicester LE1 5ET United Kingdom', 'phone': '+441162513343'}, {'name': 'Oggi', 'rating': 4.3, 'address': '161 London Road Leicester LE2 1EG United Kingdom', 'phone': '+441162545376'}], 'Burgers': [{'name': 'Byron', 'rating': 5.0, 'address': '8 Bath House Lane Highcross Leicester LE1 4SA United Kingdom', 'phone': '+441162627733'}, {'name': 'Handmade Burger Co', 'rating': 4.2, 'address': 'Highcross Lane Leicester LE1 4SD United Kingdom', 'phone': '+441162425875'}, {'name': 'Kobe Sizzlers', 'rating': 3.0, 'address': '64-66 London Road Oadby LE2 0QD United Kingdom', 'phone': '+441162225555'}], 'Japanese': [{'name': 'wagamamas', 'rating': 3.5, 'address': 'High Cross Leicester LE1 4FT United Kingdom', 'phone': '+441162629946'}, {'name': 'Little Tokyo Japanese Restaurant', 'rating': 4.2, 'address': '33 Braunstone Gate Leicester LE3 5LH United Kingdom', 'phone': '+441162857887'}], 'Steakhouses': [{'name': 'Toros Steakhouse', 'rating': 2.8, 'address': '9 Highfield Street Leicester LE2 1AB United Kingdom', 'phone': '+441162549885'}, {'name': 'Black Iron Restaurant', 'rating': 4.0, 'address': 'Hinckley Road Leicester LE3 1HX United Kingdom', 'phone': '+441163665642'}, {'name': 'Spur Steak & Grill Soaring Eagle Spur', 'rating': 4.0, 'address': 'Holiday Inn Express Filbert Way Raw Dykes Road Leicester LE2 7FL United Kingdom', 'phone': '+441162494590'}, {'name': 'Kobe Sizzlers', 'rating': 3.0, 'address': '64-66 London Road Oadby LE2 0QD United Kingdom', 'phone': '+441162225555'}], 'American': [{'name': 'Las Iguanas', 'rating': 3.6, 'address': '13-15 Belvoir Street Leicester LE1 6SL United Kingdom', 'phone': '+441162159180'}, {'name': 'Grillstock', 'rating': 2.5, 'address': "3-5 St Martin's Square Leicester LE1 5DF United Kingdom", 'phone': '+441162515224'}], 'Latin American': [{'name': 'Las Iguanas', 'rating': 3.6, 'address': '13-15 Belvoir Street Leicester LE1 6SL United Kingdom', 'phone': '+441162159180'}], 'Mexican': [{'name': 'Las Iguanas', 'rating': 3.6, 'address': '13-15 Belvoir Street Leicester LE1 6SL United Kingdom', 'phone': '+441162159180'}], 'Fish & Chips': [{'name': 'Grimsby Fisheries', 'rating': 4.5, 'address': '334 Welford Road Leicester LE2 6EH United Kingdom', 'phone': '+441162709174'}], 'Coffee & Tea': [{'name': 'KAI', 'rating': 4.6, 'address': "4 St Martin's Square Leicester LE1 5DF United Kingdom", 'phone': '+441162621400'}], 'Breakfast & Brunch': [{'name': 'KAI', 'rating': 4.6, 'address': "4 St Martin's Square Leicester LE1 5DF United Kingdom", 'phone': '+441162621400'}, {'name': 'Prana Cafe', 'rating': 5.0, 'address': '10 Horsefair street Leicester LE1 5BN United Kingdom', 'phone': '+441162547569'}], 'Sandwiches': [{'name': 'KAI', 'rating': 4.6, 'address': "4 St Martin's Square Leicester LE1 5DF United Kingdom", 'phone': '+441162621400'}], 'Seafood': [{'name': "Jimmy's Killer Prawns", 'rating': 5.0, 'address': '58A London Rd Leicester LE2 0QD United Kingdom', 'phone': '+441162230123'}, {'name': "Paddy's Marten Inn", 'rating': 5.0, 'address': '98 Martin Street Leicester LE4 6EU United Kingdom', 'phone': '+441162665123'}], 'Pubs': [{'name': 'Soar Point', 'rating': 4.5, 'address': 'The Newarke Leicester LE2 United Kingdom', 'phone': '+441162044911'}], 'Gastropubs': [{'name': 'Soar Point', 'rating': 4.5, 'address': 'The Newarke Leicester LE2 United Kingdom', 'phone': '+441162044911'}], 'Himalayan/Nepalese': [{'name': "Paddy's Marten Inn", 'rating': 5.0, 'address': '98 Martin Street Leicester LE4 6EU United Kingdom', 'phone': '+441162665123'}], 'Street Vendors': [{'name': 'Bombay Street Kitchen', 'rating': 5.0, 'address': '29 Melton Road Leicester LE4 6PN United Kingdom', 'phone': '+441162681142'}, {'name': 'Bombay Bites', 'rating': 4.0, 'address': '41A Belvoir Street Leicester LE1 6SL United Kingdom', 'phone': '+441162852299'}], 'Barbeque': [{'name': 'Grillstock', 'rating': 2.5, 'address': "3-5 St Martin's Square Leicester LE1 5DF United Kingdom", 'phone': '+441162515224'}], 'Fast Food': [{'name': "Nando's", 'rating': 3.9, 'address': '50 Granby st Leicester LE1 1DH United Kingdom', 'phone': '+441162758303'}, {'name': 'Bombay Bites', 'rating': 4.0, 'address': '41A Belvoir Street Leicester LE1 6SL United Kingdom', 'phone': '+441162852299'}], 'Chicken Shop': [{'name': "Nando's", 'rating': 3.9, 'address': '50 Granby st Leicester LE1 1DH United Kingdom', 'phone': '+441162758303'}], 'Turkish': [{'name': 'Istanbul Restaurant', 'rating': 3.0, 'address': '73 Narborough Road Leicester LE3 0LE United Kingdom', 'phone': '+441162557909'}], 'Buffets': [{'name': 'More Restaurant', 'rating': 5.0, 'address': '62a London Road Leicester LE2 0QD United Kingdom', 'phone': '+441162250000'}], 'Juice Bars & Smoothies': [{'name': 'Prana Cafe', 'rating': 5.0, 'address': '10 Horsefair street Leicester LE1 5BN United Kingdom', 'phone': '+441162547569'}], 'Spanish': [{'name': 'Barceloneta Restaurant', 'rating': 3.4, 'address': '54 Queens Road Leicester LE2 1TU United Kingdom', 'phone': '+441162708408'}], 'Basque': [{'name': 'Barceloneta Restaurant', 'rating': 3.4, 'address': '54 Queens Road Leicester LE2 1TU United Kingdom', 'phone': '+441162708408'}], 'Vegetarian': [{'name': 'The Good Earth Restaurant', 'rating': 4.3, 'address': '19 Free Lane Leicester LE1 1JX United Kingdom', 'phone': '+441162626260'}], 'Vegan': [{'name': 'The Good Earth Restaurant', 'rating': 4.3, 'address': '19 Free Lane Leicester LE1 1JX United Kingdom', 'phone': '+441162626260'}], 'Salad': [{'name': 'Kobe Sizzlers', 'rating': 3.0, 'address': '64-66 London Road Oadby LE2 0QD United Kingdom', 'phone': '+441162225555'}]}
Normalising the data and adding into pandas dataframe.
# Flatten the categories_dict into a list of businesses with their categories
flattened_data = []
for category, businesses in categories_dict.items():
for business in businesses:
# Add the category as a field in the business dictionary
business_with_category = business.copy()
business_with_category['category'] = category
flattened_data.append(business_with_category)
# Add list of businesses into a pandas DataFrame
df = pd.DataFrame(flattened_data)
# Display the dataframe
df.head()
name | rating | address | phone | category | |
---|---|---|---|---|---|
0 | Kayal | 4.7 | 153 Granby street Leicester LE1 6FE United Kin... | +441162554667 | Indian |
1 | Shivalli | 4.4 | 21 Welford Road Leicester LE2 7AD United Kingdom | +441162550137 | Indian |
2 | The Curry House | 4.5 | 118 London Road Leicester LE2 0QS United Kingdom | +441162550688 | Indian |
3 | Feast India | 3.8 | 411 Melton Road Leicester LE4 7PA United Kingdom | +441162582590 | Indian |
4 | Paddy's Marten Inn | 5.0 | 98 Martin Street Leicester LE4 6EU United Kingdom | +441162665123 | Indian |
Geocoding the business data to retrieve the longitude and latitude.
# Initialise the geocoder
geolocator = Nominatim(user_agent="myApp")
# Use a rate limiter to avoid overloading the geocoding service
geocode = RateLimiter(geolocator.geocode, min_delay_seconds=1)
# Function to geocode the address
def geocode_address(row):
location = geocode(row['address'])
if location:
return pd.Series([location.latitude, location.longitude])
df[['latitude', 'longitude']] = df.apply(geocode_address, axis=1)
df.head()
name | rating | address | phone | category | latitude | longitude | |
---|---|---|---|---|---|---|---|
0 | Kayal | 4.7 | 153 Granby street Leicester LE1 6FE United Kin... | +441162554667 | Indian | 52.632193 | -1.127243 |
1 | Shivalli | 4.4 | 21 Welford Road Leicester LE2 7AD United Kingdom | +441162550137 | Indian | 52.630902 | -1.134149 |
2 | The Curry House | 4.5 | 118 London Road Leicester LE2 0QS United Kingdom | +441162550688 | Indian | 52.627777 | -1.120299 |
3 | Feast India | 3.8 | 411 Melton Road Leicester LE4 7PA United Kingdom | +441162582590 | Indian | 52.665013 | -1.109600 |
4 | Paddy's Marten Inn | 5.0 | 98 Martin Street Leicester LE4 6EU United Kingdom | +441162665123 | Indian | 52.647132 | -1.107688 |
Sourcing the Data - Public Toilet Data¶
Retrieving the public toilet data from Open Leicester.
# public toilet locations API
toilet_response = requests.get('https://data.leicester.gov.uk/api/explore/v2.1/catalog/datasets/public-pc-locations-copy/records?limit=21')
print(toilet_response.status_code)
toilet_data = toilet_response.json()
toilet_df = pd.DataFrame(toilet_data['results'])
toilet_df.head()
200
id | uern | property_name | address | post_code | ward | property_type | sub_type | public_pcs | customer_service_pcs | available_hrs_per_week | scanners_available | byod | lat | long | location | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 1 | 2046 | Brite Centre | Braunstone Avenue, Leicester, | LE3 1LE | Braunstone Park & Rowley Fields | Community Buildings | Community Centre | 8 | 0 | 47.0 | Yes | No | 52.623022 | -1.164421 | {'lon': -1.164420537, 'lat': 52.62302183} |
1 | 1 | 2832 | Hamilton Library | 22 Maidenwell Avenue, Hamilton Way, Leicester, | LE5 1BL | Humberstone & Hamilton | Libraries | Library | 8 | 1 | 40.0 | Yes | No | 52.652641 | -1.067518 | {'lon': -1.06751775, 'lat': 52.65264058} |
2 | 1 | 2202 | Knighton Library | Clarendon Park Road, Leicester, | LE2 3AJ | Castle | Libraries | Library | 6 | 0 | 47.0 | Yes | Yes | 52.616514 | -1.118578 | {'lon': -1.118578407, 'lat': 52.6165137} |
3 | 1 | 2393 | Westcotes Library | Narborough Road, Leicester, | LE3 0BQ | Westcotes | Libraries | Library | 13 | 0 | 51.0 | Yes | No | 52.629657 | -1.148131 | {'lon': -1.148130558, 'lat': 52.62965739} |
4 | 2 | 2198 | Kingfisher Youth Centre | Neston Road/Boulder Lane, Leicester, | LE2 6RD | Saffron | Community Buildings | Youth Centre | 3 | 0 | 4.0 | No | No | 52.602284 | -1.130927 | {'lon': -1.130927431, 'lat': 52.60228379} |
Sourcing the Data - Public Wifi Data¶
Retrieving the public wifi data from Open Leicester.
# public wifi location API
wifi_response = requests.get('https://data.leicester.gov.uk/api/explore/v2.1/catalog/datasets/public-wi-fi-locations/records?limit=22')
print(wifi_response.status_code)
wifi_data = wifi_response.json()
wifi_df = pd.DataFrame(wifi_data['results'])
wifi_df.head()
200
property_name | address | post_code | ward | category | property_type | sub_type | current_ssid_broadcast | status | lat | long | position | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | Aylestone Leisure Centre | Knighton Lane East, Saffron Lane, Leicester, | LE2 6LU | Saffron | Operational | Sport Centres & Pools,inc. Pavilions etc | Leisure Centre | The_Cloud | Complete | 52.610546 | -1.134692 | {'lon': -1.134692354, 'lat': 52.61054616} |
1 | Cossington Street Sports Centre | Cossington Street, , Leicester, | LE4 6JD | Belgrave | Operational | Sport Centres & Pools,inc. Pavilions etc | Leisure Centre | The_Cloud | Complete | 52.650776 | -1.119975 | {'lon': -1.119974754, 'lat': 52.6507762} |
2 | Saffron Lane Sports Centre | Saffron Lane, Aylestone Road, Leicester, | LE2 7NE | Saffron | Operational | Sport Centres & Pools,inc. Pavilions etc | Leisure Centre | The_Cloud | Complete | 52.613127 | -1.136518 | {'lon': -1.136517743, 'lat': 52.61312727} |
3 | Spence Street Baths And Sports Centre | Spence Street, St Barnabas Road, Leicester, | LE5 3NW | North Evington | Operational | Sport Centres & Pools,inc. Pavilions etc | Leisure Centre | The_Cloud | Complete | 52.639520 | -1.099496 | {'lon': -1.099495884, 'lat': 52.63951956} |
4 | Belgrave Library | Cossington Street, , Leicester, | LE4 6JD | Belgrave | Operational | Libraries | Library | LeicesterLibraries Only | Complete | 52.650870 | -1.120505 | {'lon': -1.120505029, 'lat': 52.65087004} |
Sourcing the Data - Library Data¶
Retrieving the library data from Open Leicester.
# library location in leicester API
library_response = requests.get('https://data.leicester.gov.uk/api/explore/v2.1/catalog/datasets/library-opening-hours/records?limit=20')
print(library_response.status_code)
library_data = library_response.json()
library_df = pd.DataFrame(library_data['results'])
library_df.head()
200
library | address | ward | monday_hours | tuesday_opening_hours | wednesday_opening_hours | thursday_opening_hours | friday_opening_hours | saturday_opening_hours | sunday_opening_hours | location | |
---|---|---|---|---|---|---|---|---|---|---|---|
0 | Beaumont Leys Library | Beaumont Way, Leicester, LE4 1DS | Beaumont Leys | 9.00am - 6.30pm | 9.00am - 6.30pm | 9.00am - 6.30pm | 9.00am - 6.30pm | 9.00am - 5.00pm | 9.30am - 1.00pm | Closed | {'lon': -1.165460107, 'lat': 52.66705379} |
1 | Brite Centre | Braunstone Avenue, Leicester, LE3 1LE | Braunstone Park & Rowley Fields | 9.00am - 5.00pm | 9.00am - 7.00pm | 9.00am - 5.00pm | 9.00am - 7.00pm | 9.00am - 5.00pm | 10.00am - 4.00pm | Closed | {'lon': -1.164420537, 'lat': 52.62302183} |
2 | Highfields Library | 98 Melbourne Road, Leicester, LE2 0DS | Wycliffe | 9.00am - 6.00pm | 9.00am - 6.00pm | 9.00am - 6.00pm | 9.00am - 6.00pm | 9.00am - 6.00pm | 10.00am - 4.00pm | Closed | {'lon': -1.114541769, 'lat': 52.63284542} |
3 | Belgrave Library | Cossington Street, Leicester, LE4 6JD | Belgrave | 10.00am - 7.00pm | 10.00am - 7.00pm | 10.00am - 7.00pm | 10.00am - 7.00pm | 10.00am - 7.00pm | 10.00am - 4.00pm | 12.00pm - 4.00pm | {'lon': -1.120505029, 'lat': 52.65087004} |
4 | Knighton Library | Clarendon Park Road, Leicester, LE2 3AJ | Castle | 10.00am - 6.30pm | 10.00am - 6.30pm | 10.00am - 6.30pm | 10.00am - 6.30pm | 10.00am - 5.00pm | 10.00am - 4.00pm | Closed | {'lon': -1.118578407, 'lat': 52.6165137} |
Normalising the data to view the data structure and dropping columns that are not required.
# Extracting 'lat' and 'lon' into their own columns
library_df['latitude'] = library_df['location'].apply(lambda x: x.get('lat') if isinstance(x, dict) else None)
library_df['longitude'] = library_df['location'].apply(lambda x: x.get('lon') if isinstance(x, dict) else None)
# Drop the original 'location' column, no longer needed
library_df = library_df.drop('location', axis=1)
library_df.head()
library | address | ward | monday_hours | tuesday_opening_hours | wednesday_opening_hours | thursday_opening_hours | friday_opening_hours | saturday_opening_hours | sunday_opening_hours | latitude | longitude | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | Beaumont Leys Library | Beaumont Way, Leicester, LE4 1DS | Beaumont Leys | 9.00am - 6.30pm | 9.00am - 6.30pm | 9.00am - 6.30pm | 9.00am - 6.30pm | 9.00am - 5.00pm | 9.30am - 1.00pm | Closed | 52.667054 | -1.165460 |
1 | Brite Centre | Braunstone Avenue, Leicester, LE3 1LE | Braunstone Park & Rowley Fields | 9.00am - 5.00pm | 9.00am - 7.00pm | 9.00am - 5.00pm | 9.00am - 7.00pm | 9.00am - 5.00pm | 10.00am - 4.00pm | Closed | 52.623022 | -1.164421 |
2 | Highfields Library | 98 Melbourne Road, Leicester, LE2 0DS | Wycliffe | 9.00am - 6.00pm | 9.00am - 6.00pm | 9.00am - 6.00pm | 9.00am - 6.00pm | 9.00am - 6.00pm | 10.00am - 4.00pm | Closed | 52.632845 | -1.114542 |
3 | Belgrave Library | Cossington Street, Leicester, LE4 6JD | Belgrave | 10.00am - 7.00pm | 10.00am - 7.00pm | 10.00am - 7.00pm | 10.00am - 7.00pm | 10.00am - 7.00pm | 10.00am - 4.00pm | 12.00pm - 4.00pm | 52.650870 | -1.120505 |
4 | Knighton Library | Clarendon Park Road, Leicester, LE2 3AJ | Castle | 10.00am - 6.30pm | 10.00am - 6.30pm | 10.00am - 6.30pm | 10.00am - 6.30pm | 10.00am - 5.00pm | 10.00am - 4.00pm | Closed | 52.616514 | -1.118578 |
Data Cleaning¶
Data (from resturant data, public wifi data, public toilet data and library data) are removed if the latitude and longitude columns have missing values.
# Drop the rows if they have missing values
df = df.dropna(subset=['latitude', 'longitude'])
toilet_df = toilet_df.dropna(subset=['lat', 'long'])
wifi_df = wifi_df.dropna(subset=['lat', 'long'])
library_df = library_df.dropna(subset=['latitude', 'longitude'])
Interactive Map - 4¶
The map intention is to display different resturants locations and public facilities (libraries, wifi, public tiolet) using markers. Popup infomation is display when the marker is clicked on, different display icon and colours used on the each marker to differentiate between markers. Clustering is used on the resturant markers to help improve the readabilty of the map and a jitter function to make sure the markers are not overlapping with other markers.
# Function to add a small random jitter to latitude and longitude so that the markers doenst overlap with each other
def add_jitter(lat, lon, range=0.0001):
return lat + np.random.uniform(-range, range), lon + np.random.uniform(-range, range)
# Function to create popup messages for markers
def create_popup(row, category):
if category == 'restaurant':
details = f"<b>Name:</b> {row['name']}<br><b>Address:</b> {row['address']}<br><b>Rating:</b> {row['rating']}<br><b>Phone:</b> {row['phone']}"
if 'review' in row and pd.notnull(row['review']):
details += f"<br><b>Review:</b> {row['review']}"
elif category == 'toilet':
details = f"<b>Public Toilet:</b> {row['property_name']}<br><b>Address:</b> {row['address']}"
elif category == 'wifi':
details = f"<b>Wi-Fi Location:</b> {row['property_name']}<br><b>Address:</b> {row['address']}"
elif category == 'library':
details = f"<b>Library:</b> {row['library']}<br><b>Address:</b> {row['address']}"
else:
details = "Information not available"
return folium.Popup(details, max_width=300)
# Start the map
map_center = [52.6369, -1.1398]
mymap = folium.Map(location=map_center, zoom_start=13, min_zoom=11)
# Plotting Restaurants with jitter
restaurants = plugins.MarkerCluster(name='Restaurants').add_to(mymap)
for idx, row in df.iterrows():
jittered_lat, jittered_lon = add_jitter(row['latitude'], row['longitude'])
folium.Marker(
[jittered_lat, jittered_lon],
popup=create_popup(row, 'restaurant'),
icon=folium.Icon(icon='cutlery', prefix='fa', color='blue')
).add_to(restaurants)
# Plotting Public Toilets with jitter
toilets = folium.FeatureGroup(name='Public Toilets', show=True)
for idx, row in toilet_df.iterrows():
jittered_lat, jittered_lon = add_jitter(row['lat'], row['long'])
folium.Marker(
[jittered_lat, jittered_lon],
popup=create_popup(row, 'toilet'),
icon=folium.Icon(icon='toilet', prefix='fa', color='lightblue')
).add_to(toilets)
toilets.add_to(mymap)
# Plotting Public Wi-Fi Locations with jitter
wifi = folium.FeatureGroup(name='Public Wi-Fi', show=True)
for idx, row in wifi_df.iterrows():
jittered_lat, jittered_lon = add_jitter(row['lat'], row['long'])
folium.Marker(
[jittered_lat, jittered_lon],
popup=create_popup(row, 'wifi'),
icon=folium.Icon(icon='signal', prefix='fa', color='green')
).add_to(wifi)
wifi.add_to(mymap)
# Plotting Libraries with jitter
libraries = folium.FeatureGroup(name='Libraries', show=True)
for idx, row in library_df.iterrows():
jittered_lat, jittered_lon = add_jitter(row['latitude'], row['longitude'])
folium.Marker(
[jittered_lat, jittered_lon],
popup=create_popup(row, 'library'),
icon=folium.Icon(icon='book', prefix='fa', color='red')
).add_to(libraries)
libraries.add_to(mymap)
# Add LayerControl to toggle each category on and off
folium.LayerControl().add_to(mymap)
mymap.save('map4.html')
mymap
Final Map¶
The design concept of the final map is to enable the user to have a quick overview of the areas in Leicester. The aim of the map is to provide an easy-to-navigate and comprehensive visual representation of essential information that can help newcomers make informed decisions about moving to or visiting the city.
The combination of map 3 and 4 design ideas lead to produce this final map. The user is able to independantly toggle different layers and markers display different demographic information, resturants and public facilities. Popup information is displayed when the marker is clicked on, similar with the cloropleth demographic information. This enables users to evaluate the convenience and accessibility of essential services within each area.
The zoom level is set to 'zoom_start=12, min_zoom=11' parameter, it allows the user to focus on a specific scale of the map to guide the user. It allows the map to be open at a meaningful zoom level and also restrist the user from zooming out too far which can lose the context of visualising the data.
The design aspect is the same as mentioned on map 3 and 4.
# Start the map with Leicester location
map = folium.Map(location=[52.6369, -1.1398], zoom_start=12, min_zoom=11)
# Define color maps for each geographic data type
population_colormap = cm.linear.YlOrRd_09.scale(population_df['PopulationDensity'].min(), population_df['PopulationDensity'].max())
age_colormap = cm.linear.YlGn_09.scale(average_age_df['AverageAge'].min(), average_age_df['AverageAge'].max())
unemployment_colormap = cm.linear.Blues_09.scale(unemployment_rate_df['UnemploymentValue'].min(), unemployment_rate_df['UnemploymentValue'].max())
# Function to create a GeoJson layer
def create_geojson_layer(data, colormap, field_name, layer_name):
# function to style the each feature
def style_function(feature):
value = feature['properties'].get(field_name, 0)
return {
'fillColor': colormap(value) if value else 'transparent',
'color': 'black',
'weight': 1,
'fillOpacity': 0.5
}
# function to highlight the feature when hover
def highlight_function(feature):
return {
'fillColor': '#ff0000',
'color': 'black',
'weight': 2,
'fillOpacity': 0.6
}
# tooltip to show information when hover
tooltip = folium.GeoJsonTooltip(
fields=['electoral_wards_and_divisions', field_name],
aliases=['Ward:', layer_name + ':'],
localize=True
)
# create the layer to add to map
layer = folium.GeoJson(
data,
style_function=style_function,
highlight_function=highlight_function,
tooltip=tooltip,
name=layer_name
)
colormap.add_to(map)
layer.add_to(map)
# Create and add GeoJson layers for geographic data to the map
create_geojson_layer(geojson_data, population_colormap, 'PopulationDensity', 'Population Density')
create_geojson_layer(geojson_data, age_colormap, 'AverageAge', 'Average Age')
create_geojson_layer(geojson_data, unemployment_colormap, 'UnemploymentValue', 'Unemployment Rate')
# Function to add jitter to lat, lon for markers
def add_jitter(lat, lon, range=0.0001):
return lat + np.random.uniform(-range, range), lon + np.random.uniform(-range, range)
# Function to create popup for point data markers
def create_popup(row, category):
details = "<b>Category:</b> " + category.title() + "<br>"
if 'name' in row:
details += "<b>Name:</b> " + str(row['name']) + "<br>"
if 'address' in row:
details += "<b>Address:</b> " + str(row['address']) + "<br>"
if 'rating' in row:
details += "<b>Rating:</b> " + str(row['rating']) + "<br>"
if 'phone' in row:
details += "<b>Phone:</b> " + str(row['phone']) + "<br>"
return folium.Popup(details, max_width=300)
# Function to add markers to the map
def add_markers_to_map(df, category, icon, color, lat_col='latitude', lon_col='longitude'):
for _, row in df.iterrows():
jittered_lat, jittered_lon = add_jitter(row[lat_col], row[lon_col])
popup = create_popup(row, category)
folium.Marker(
location=[jittered_lat, jittered_lon],
popup=popup,
icon=folium.Icon(icon=icon, color=color)
).add_to(map)
# Function to display different layers of markers
def add_point_data_layer(df, category, icon, color, lat_col='latitude', lon_col='longitude'):
feature_group = folium.FeatureGroup(name=category, show=False)
for idx, row in df.iterrows():
jittered_lat, jittered_lon = add_jitter(row[lat_col], row[lon_col])
folium.Marker(
[jittered_lat, jittered_lon],
popup=create_popup(row, category.lower()),
icon=folium.Icon(icon=icon, prefix='fa', color=color)
).add_to(feature_group)
feature_group.add_to(map)
# add the geojson layer for each dataframe
add_point_data_layer(df, 'Restaurants', 'cutlery', 'blue')
add_point_data_layer(toilet_df, 'Public Toilets', 'toilet', 'lightblue', 'lat', 'long')
add_point_data_layer(wifi_df, 'Public Wi-Fi', 'signal', 'green', 'lat', 'long')
add_point_data_layer(library_df, 'Libraries', 'book', 'red', 'latitude', 'longitude')
# Add LayerControl to toggle layers
folium.LayerControl().add_to(map)
map.save('map5.html')
map