6
Minutes read

Measuring walkability of Greater Kuala Lumpur

No items found.
Authors
Khoo Wei Yang
No items found.
Key Takeaways
Data Sets Overview

A multi-year research project analyzing mobility patterns, public transport performance, and accessibility in Greater KL using large-scale administrative, geospatial and qualitative data.

measuring-walkability-of-greater-kuala-lumpur
Data Visualisations
Disclaimer
As we transition to a digital-first communication and continue building our knowledge hub, publications released before October 2025 are preserved in their original format. Publications released from October 2025 onward adopt a new, digitally friendly format for easier online reading. The official versions of earlier publications, including their original language and formatting, remain available in the downloadable PDF.

Why walkable cities?

Walkability is a core urban design element. Pedestrian infrastructure often lays the foundation for basic mobility in urban spaces, it is also the bridge that connects other modes of transport. In Kuala Lumpur, moving around by car has become the default, if not the only mode of mobility. However, walking should not be an afterthought.

Walking is not just another means of getting around alternative to cars. Besides being the most practical and sustainable form of mobility, walking promotes regular physical activity for improved health outcomes [@leeImportanceWalkingPublic2008], reduced environmental pollution, among other benefits.

Streets built for walking are essential to a livable city. Urbanist Jane Jacobs believed in the importance of sidewalks and pedestrian activity as generators of diversity, suggesting that uses of sidewalk in shaping safety, and contact among people [@jacobsDeathLifeGreat1992]. In her conception, well-designed, walkable streets are not only key to making people walk, but a living part of the city.

Higher walkability in cities has shown positive effects on health, quality of life and the environment [@baobeidWalkabilityItsRelationships2021]. Neighborhoods with higher walkability ratings tend to have higher property values and more vibrant local businesses [@chernobaiEffectWalkabilityHouse2022].

However, walkability can be a difficult object to measure, test and design. A range of elements compose a walkable city. For pedestrians, some immediately perceptible built elements such as the presence of sidewalks and crosswalks, shades, street lighting and traffic-calming measures come to mind. Besides infrastructure completeness, overall design and layout of built environment of a city and its amenities also influences the foot-accessibility to destinations such as schools and shops. These factors can vary in different parts of the city, from one street to the next, and experienced differently from one person to the other.

For instance, Kuala Lumpur is known for its walkability problems, but just how un-walkable it is? Asking different people who access and/or live in different parts of the city might yield different answers. Our knowledge of spatial attributes of the city is inconsistent, making it challenging to create an overall picture of city walkability and a single, definitive goal of walkability for the city.

To systematically understand a city's walkability, we must first conceptualise walkability. Despite thrown about by practitioners, researchers, policy-makers and the public, there is no agreed upon definition of the concept [@tobinRethinkingWalkabilityDeveloping2022].

In the urban design literature, walkability is often understood in terms of urban morphology — the physical form and structure of urban areas. A few spatial measures commonly used to understand urban walkability are (1) permeability and (2) connectivity.

This article seek to explore and illustrate the use of urban morphology of Greater Kuala Lumpur to understand walkability of the city. To do this, I collect and analyse the street network data of Greater KL through OSMnx and pandana, python packages that enable is to model and analyse geospatial data from OpenStreetMap, an open-source map database.

Permeability

Streets of a city represent a type of spatial network, which can be represented in graph. Graphs are made up of edges and nodes (vertices). Intersections are nodes that connect at least two edges, which represent streets.

Intersection density measures the number of intersections in street network per unit area. We use street intersections to proxy permeability. Permeability describes the extent to which urban forms permit mobility in different directions.

Three city street networks with varying levels of permeability
Source: Jacobs, A. B. (1993)

The figure above shows three city street networks with varying levels of permeability. An area with higher intersection density requires less out of direction travel to get from point A to point B, so distances are shorter and more conducive to taking trips without a car.

First, we import the necessary Python libraries:

#| output: false
#| message: false

import osmnx as ox
import matplotlib.pyplot as plt
import geopandas as gpd
import seaborn as sns
import numpy as np
import pandas as pd
import pandana as pnda
# select city and crs
cityname = 'kuala lumpur, malaysia'
crs = 32647
# obtain graph of the city
graph = ox.graph_from_place(cityname, network_type="walk")

# project graph onto our crs
graph = ox.projection.project_graph(graph, to_crs=crs)
# simplify our graph to exclude non-intersections
graph_simplified = ox.simplification.consolidate_intersections(
    graph,
    tolerance=5, # buffer around each node (project the graph beforehand)
    rebuild_graph=True,
    dead_ends=False, # remove cul-de-sacs
    reconnect_edges=True
)
# obtain nodes and edges and pass it as gdfs
nodes, edges = ox.graph_to_gdfs(graph)
nodes_s, edges_s = ox.graph_to_gdfs(graph_simplified)

@fig-graphkl depicts the graph representation of the street networks of Kuala Lumpur. At first glance, we observe that the street designs are closely adapted to its terrain. Unlike the gridiron plan of New York and Kyoto, Kuala Lumpur follows an organic layout that is responsive to its topography, leading to curvilinear streets and irregular lots. The extent of street network is also limited by some areas with forest reserves and/or hill fomrations.

#| label: fig-graphkl
#| fig-cap: "A graph representation of Kuala Lumpur"

# plot a base graph of the street network of kuala lumpur
fig, ax = plt.subplots(figsize=(20,15))
ax.set_aspect('equal')
ax.set_facecolor('black')
fig.set_facecolor('black')

edges.plot(
    ax=ax,
    color='white',
    linewidth=0.2
)

plt.tight_layout()

plt.savefig('/output/kl_graph.png')

A heatmap of intersection density

To obtain the intersection density of the city, we divide the entire street network into equally sized area. We then count up the number of intersection in each area. @fig-intrscthexmap shows the intersection per hexagonal area.

#| label: fig-intrscthexmap
#| fig-cap: "Intersection counts per hexbin"

# setting plot style parameters
COLOR = 'white' 
plt.rcParams['text.color'] = COLOR
plt.rcParams['axes.labelcolor'] = COLOR
plt.rcParams['xtick.color'] = COLOR
plt.rcParams['ytick.color'] = COLOR

fig, ax = plt.subplots(figsize=(20,15))
ax.set_aspect('equal')
fig.set_facecolor('black')
ax.set_facecolor('black')

hb = ax.hexbin(
    x=nodes_s['x'],
    y=nodes_s['y'],
    gridsize=[75,65],
    cmap='viridis',
    mincnt=1,
    vmax=70,
)

cb = plt.colorbar(hb, ax=ax, shrink=0.4, ticks=[1, 20, 40, 60, 80, 100])
cb.ax.tick_params(color='none', labelsize=20)
cb.ax.set_yticklabels(['1', '20', '40', '60', '80', '>= 100'])
cb.set_label('Intersections per hexagon', fontsize=20, fontweight='bold')

plt.tight_layout()

plt.savefig('/output/intersection_hexbin.png')

From @fig-intrscthexmap, we see that intersection-dense areas are concentrated around the Central Business District (CBD), surrounded by Jalan Tun Razak. This area include places such as Pasar Seni, Kampung Baru, Bukit Bintang and the like. Small disconnected spots of highly intersected areas can also be found in Bangsar-Universiti Malaya, and Kepong area.

The uneven intersection densities across the city mean that certain areas are not as permeable as others, making getting around in those areas more difficult by walking. Whereas accessing places in areas with higher permeability is easier by foot. Apart from that, moving from one place to the next in the same area is also easier by foot, further incentivising walking within these areas. It is no wonder why the CBD area is known for its tourism and dense office buildings.

import plotly.express as px
import plotly.subplots as sp
import plotly.io as pio
# get the binned data from the hexbin object
counts = hb.get_array()
verts = hb.get_offsets()

counts_unmasked = counts.filled(np.nan)
int_counts = np.array([round(i) for i in counts_unmasked])
#| label: fig-histhexbin
#| fig-cap: Histogram of intersection count hexbin distribution
pio.renderers.default = "notebook"
# plot histogram
intersection_dist = px.histogram(int_counts,
                                 labels={"value": "intersections in each bin"})
intersection_dist.update_layout(showlegend=False)

@fig-histhexbin shows the distribution of the number of intersections across all binned intersection (each observation is one bin attached to a value of intersection counts). As a whole, the city doesn't appear to be evenly permeable — a majority of unit areas found in the city street network contains zero to ten intersections. Each equally-sized area of the city has, on average, roughly 8.27 intersections.

int_counts.mean().round(3)
8.269

Accessibility

Knowing how permeable a city is might not tell us a great deal about walkability. The spatial distribution of places of interest (POIs) also plays an important role. It is crucial that the design of urban spaces can ensure that sociable places and key amenities like parks, libraries, shops, schools, and hospitals are within reach of walking distance. But what does this mean exactly?

Carlos Moreno popularised the “15-minute city” concept, an urban design goal that emphasises proximity. A short definition of the concept reads:

The 15-minute city represents an urban model in which the essential needs of residents are accessible on foot or by bicycle within a short perimeter in high-density areas.

[@moreno15MinuteCitySolution2024]

The model suggests that travel time contributes to urban walkability. People are more likely to walk if destinations are a stone throw away. When key amenities are strategically placed within 15-minute radii, people are more likely to walk. A study conducted by MIT researchers found strong correlation between the accessibility of amenities within a 15-minute walk from home and the usage of those amenities [@abbiasov15MinuteCityQuantified2022]. Simply by having places that are close to each other can improve visits to and usage of those places. A high variety of places that are close to each other incentivises people to walk in taking journeys and performing their daily activities.

Measuring walking time to destinations

Proximity is a function of network distance — the shortest path or the lowest cost-time to travel between two locations, considering the network's structure and constraints. By analysing the city's street as networks, we can measure how walkable to each POI is within the city.

Each street of a city is an edge in a graph. Walking at a constant speed, each street has a travel time based on its length, we give each edge a travel time as its weight. Then we can compute how long it takes to walk 5, 10, 15 minutes away from each node.

In a graph, we can calculate how much time is spent walking down each edge to reach a node and in reverse, find how far we can reach within a map from that node.

To find how much time it takes to walk to any nearest POI in a city, we can reverse the calculation, by calculating how far you can reach away from the POI. However, it would be difficult to approximate how far you can walk from any POI without a reference point on the network. We can take any intersection (nodes on our network) as reference.

As the POIs of our interest may not lie directly on the network (for example, the exact coordinate of a hospital can be a little far from the streets, even though it is walkable from the street), we can approximate their position on the network by proxying it to the nearest intersection to the POI.

Thus, we can find how many intersections can be reach within a defined time if we start from the POI (now proxied at an intersection), and we will have an approximatation of how walkable is it to the nearest POI within any area in the city.

# collect metro station points from osmnx

place = "Selangor"
stations = ox.features_from_place(place, tags = {'railway':['station']})
stations = stations[stations['name'].notnull()]
#| output: false

# ensure all geometries are points
stations["geometry"] = stations.geometry.centroid
#| label: tbl-stations
#| tbl-cap: Collected centroids of train and metro stations in Greater KL

stations

Metric 1 2 3 4 5 6 7 8 9 10
Count 69,463 69,463 69,463 69,463 69,463 69,463 69,463 69,463 69,463 69,463
Mean 322.32 470.08 561.38 628.41 675.33 713.01 742.60 764.89 782.85 796.74
Std Dev 232.88 248.35 245.64 236.79 227.51 216.40 204.76 195.58 185.41 176.28
Min 0.00 0.00 0.00 0.00 26.80 28.37 60.50 81.15 93.34 104.81
25th % 143.35 270.63 361.70 437.78 500.52 562.32 617.41 666.47 708.71 747.76
Median 269.14 433.36 544.58 646.10 724.22 799.94 865.36 900.00 900.00 900.00
75th % 448.10 654.50 799.37 900.00 900.00 900.00 900.00 900.00 900.00 900.00
Max 900.00 900.00 900.00 900.00 900.00 900.00 900.00 900.00 900.00 900.00

#| label: fig-proxplot
#| fig-cap: "Heatmap of walking time to nearest POIs"

# plot the walking time to nearest POIs
fig, ax = plt.subplots(figsize=(12,15))
ax.set_axis_off()
ax.set_aspect('equal')
fig.set_facecolor((0,0,0))

# plot distance to nearest POI
sc = ax.scatter(
    x=nodes['x'],
    y=nodes['y'],
    c=distances[1],
    s=1,
    cmap='viridis_r',
)

cb = fig.colorbar(sc, ax=ax, shrink=0.4, ticks=[0, 300, 600, 900])
cb.ax.tick_params(color='none', labelsize=20)
cb.ax.set_yticklabels(['0', '5', '10', '>= 15'])
cb.set_label('Walking time to nearest POI (minutes)', fontsize=20, fontweight='bold')

plt.tight_layout()

plt.savefig('/output/walk_access.png')

We collect the coordinates of amenity POIs like schools, hospitals, libraries, public services, townhalls, community centres, malls and parks throughout the city. We then look at the nearest 10 POIs from any POI.

@fig-proxplot shows the amount of time it takes to walk to the nearest ten POIs from any point on the street network. The brighter areas indicate that more POIs can be reached within shorter walking time. Being in the dimmer areas require 15 minutes or more to reach the nearest POI (in all directions). Brighter areas also signifies higher densities of POIs, making it easier to get from one to another in those areas.

Walkable POIs are distributed in patchwork throughout the city. Areas where walking to another POI takes more than 15 minutes are littered across and in between areas where the same walking takes shorter time. These darker patches could be geographically or topologically determined (e.g., forest reserves or river systems). Visually gauging the base graph ( @fig-graphkl ), we can conclude that this is the case generally.

Multi-modal connectivity

Walking is also complemented by other modes of transport. Most public transport journeys start and end with walk [@uitpWhyWalkingPublic2024]. Pedestrian-accessible public transport systems reduce the frequency of changing between modes, making journeys in the city frictionless.

To reduce car usage, sustainable public transport policies should promote pedestrian access to public transport. Currently, public transport connectivity outside the KL city centre is addressed by providing "Park-and-ride" option, allowing commuters to drive from nearby residential suburbs to Light Rail, Mass Rapid Transit or Land Commuter stations. This solution works to reduce the inbound vehicle traffic to city centre, but served to divert traffic elsewhere.

The solution only address the short-term problem of traffic management (and mostly inbound traffic). As more and more of the city outskirts develop into residential suburbs, and further into urban areas of their own right, traffic flow worsens in these areas, even creating problems of their own [@chanAuthoritiesUrgedAddress2025].

Direct foot access to public transport is key to increase public transport usage, this means making access to public transport points like metro and bus stations walkable. Like the exercise above, we can assess the walkability of public transport points by finding the walking time on the street networks towards each public transport point. To do this, we can rely on isochrones.

# collect metro station points from osmnx

place = "Selangor"
stations = ox.features_from_place(place, tags = {'railway':['station']})
stations = stations[stations['name'].notnull()]
#| output: false

# ensure all geometries are points
stations["geometry"] = stations.geometry.centroid

#| label: tbl-stations
#| tbl-cap: Collected centroids of train and metro stations in Greater KL

stations

Isochrones visually represent the travel time to a point in a network. Isochrones are typically computed by generating shortest-path trees on graphs, then generating a convex hull around the accessible POI. Put differently, each concentric ring of the isochrone represent the further extent one can reach by walking within a fixed duration. The wider the isochrones, the further one can walk out of direction from that point within the same time, making it more accessible by walking. For example, taking five minutes to walk to a train station

We draw isochrones around a random sample of LRT, MRT and KTM stations in Greater Kuala Lumpur to produce the average out of direction travel distance, based on 5 minutes, 10 minutes and 15 minutes on foot, assuming the same walk speed. To do this, we use the openreouteservice API provided by HeiGIT. We visualise this in @fig-stationisochrones.

#| output: false
#| message: false

import folium
from shapely.geometry import shape

import os
import time
from dotenv import load_dotenv
import openrouteservice
from openrouteservice import client

load_dotenv()  # reads .env

ORS_API_KEY = os.getenv("ORS_API_KEY")
client = openrouteservice.Client(key=ORS_API_KEY)

# get the first station to initialize the map
first_station = stations.iloc[0]
center_lat, center_lon = first_station.geometry.y, first_station.geometry.x

# define a folium map
m = folium.Map(
    location=[center_lat, center_lon],
    zoom_start=13,
    tiles="cartodbpositron"
)

#| output: false

# iterate over all stations
walk_time = [300, 600, 900]
colors_map = [ "#2ca02c", "#1f76b4a0", "#ff7f0e",]  # example hex colors

for _, station in stations.sample(50).iterrows():
    lon, lat = station.geometry.x, station.geometry.y
    # plot isochrones using openrouteservice
    iso = client.isochrones(
        locations=[[lon, lat]],
        profile="foot-walking",
        range=walk_time,
    )

    iso_gdf = gpd.GeoDataFrame(
        [{"geometry": shape(f["geometry"]), "value": f["properties"]["value"]} for f in iso["features"]],
        crs="EPSG:4326"
    )
    # plot each isochrone with its corresponding color
    for val, color in zip(walk_time, colors_map):
        subset = iso_gdf[iso_gdf["value"] == val]
        folium.GeoJson(
            subset,
            style_function=lambda feature, c=color: {
                "fillColor": c,
                "color": c,
                "fillOpacity": 0.3,
                "weight": 1
            }
        ).add_to(m)

    folium.Marker([lat, lon], popup=station["name"]).add_to(m)

    time.sleep(2)  # avoid rate limit
    
#| label: fig-stationisochrones
#| fig-cap: Isochrones of walking time to random sample of stations by 5, 10 and 15 minutes

m

A birds-eye-view of walkability

Now that we have an idea of what walkability looks like in Greater KL using urban morphological measures, let's put together an index to compare across localities in Greater KL. We use the same measures of permeability based on overall intersection density, and accessibility based on POI proximity to derive a simple composite score of walkability. We compare walkability of localities of the Greater KL conurbation and create a harmonised index using this score.

@fig-walkabilityidx shows the ranking of localities by walkability.

def calculate_poi_accessibility(localities, tags):
    """
    Calculates reachable POIs within 10 mins walk.
    """
    WALK_TIME_MIN = 10
    MAX_DIST_SEC = WALK_TIME_MIN * 60
    all_walktime = []

    for locality in localities:
        try:
            # 1. Get Street Network and project it
            G = ox.graph_from_place(locality, network_type="walk")
            G_proj = ox.project_graph(G)
            nodes_proj, edges_proj = ox.graph_to_gdfs(G_proj)
            
            # 2. Get POIs and project to match G
            pois = ox.features_from_place(locality, tags=tags)
            if pois.empty:
                continue
            pois_proj = pois.to_crs(nodes_proj.crs)
            
            # 3. Add travel times
            # Note: add_edge_speeds expects unprojected usually, 
            # or use G_proj but ensure 'length' is in meters.
            G_proj = ox.routing.add_edge_speeds(G_proj, fallback=4.5)
            G_proj = ox.routing.add_edge_travel_times(G_proj)
            nodes = ox.graph_to_gdfs(G_proj, edges=False)[['x', 'y']]
            edges = ox.graph_to_gdfs(G_proj, nodes=False).reset_index()[['u', 'v', 'travel_time']]
            
            print(edges['travel_time'].describe())

            # 4. Pandana Network
            network = pnda.Network(
                node_x=nodes['x'],
                node_y=nodes['y'],
                edge_from=edges['u'],
                edge_to=edges['v'],
                edge_weights=edges[['travel_time']]
            )

            # 5. Pre-calculate nearest nodes for POIs to avoid redundant lookups
            # We use the projected coordinates
            # Extract centroids from the pois' geometries
            centroids = pois_proj.centroid
            poi_nearest_nodes = ox.nearest_nodes(G_proj, X=centroids.x, Y=centroids.y)
            
            # Link POIs to the network
            network.set_pois(
                category='my_pois',
                maxdist=MAX_DIST_SEC,
                maxitems=300,
                x_col=centroids.x,
                y_col=centroids.y
            )

            # 6. Calculate reachability
            # This returns travel times to the Nth nearest POI
            reachable_pois_df = network.nearest_pois(
                distance=MAX_DIST_SEC,
                category='my_pois',
                num_pois=300
            )            
    
            is_under_limit = reachable_pois_df < MAX_DIST_SEC
            node_counts = is_under_limit.sum(axis=1)
            poi_reachable_counts = pd.Series(poi_nearest_nodes).map(node_counts)
           
            poi_scores_df = pd.DataFrame({
                'locality': locality,
                'POI_ID': pois.index,
                'Reachable_POIs_Count': poi_reachable_counts.values - 1 # Subtract 1 for self-reachability
            })                       

            all_walktime.append(poi_scores_df)
 
        except Exception as e:
            print(f"Error in {locality}: {e}")

    return all_walktime

def get_intersection_density(localities):
    """
    Get intersection density stats from OSMnx for all localities.
    """
    all_intersection_stats = []

    for locality in localities:
        try:
            # get street network and project it
            G = ox.graph_from_place(locality, network_type="walk")
            G_proj = ox.project_graph(G)
            nodes_proj, edges_proj = ox.graph_to_gdfs(G_proj)
            
            # get geocode gdf for area calculation
            gdf = ox.geocode_to_gdf(locality)
            gdf_proj = ox.projection.project_gdf(gdf)

            area_m2 = gdf_proj.geometry.area.iloc[0]

            # get graph stats
            graph_stats = ox.basic_stats(G_proj, area = area_m2)
            intersection_stats = pd.DataFrame({
                'locality': [locality],
                'area_m2': [area_m2],
                'intersection_count' : [graph_stats['intersection_count']],
                'intersection_density_km' : [graph_stats['intersection_density_km']],
                'street_density_km' : [graph_stats['street_density_km']]
            })

            all_intersection_stats.append(intersection_stats)

        except Exception as e:  
            print(f"Error in {locality}: {e}")
    return all_intersection_stats

gkl_localities = [
    "Kuala Lumpur, Malaysia",
    "Putrajaya, Malaysia",

    "Petaling Jaya, Selangor, Malaysia",
    "Shah Alam, Selangor, Malaysia",
    "Subang Jaya, Selangor, Malaysia",
    "Klang, Selangor, Malaysia",

    "Ampang Jaya, Selangor, Malaysia",
    "Kajang, Selangor, Malaysia",
    "Selayang, Selangor, Malaysia",
    "Rawang, Selangor, Malaysia",

    "Sepang, Selangor, Malaysia",
    "Kuala Langat, Selangor, Malaysia",
    "Kuala Selangor, Selangor, Malaysia"
    ]

# select points of interest based on osm tags
tags = {
    'amenity':['school',
               'college',
               'university',
               'library',
               'hospital',
               'public_service',
               'post_office',
               'townhall',
               'community_centre'],
    'shop':['mall'],
    'leisure':['park']
}

#| output: false
gkl_intersection_stats = get_intersection_density(gkl_localities)

Error in Rawang, Selangor, Malaysia: Found no graph nodes within the requested polygon.

#| label: tbl-intersectionstats
#| tbl-cap: Permeablity statistics by Greater KL locality
 
permeability_score = pd.concat(gkl_intersection_stats, ignore_index = True)
permeability_score

Locality Area (km²) Intersections Intersection Density/km² Street Density/km
Kuala Lumpur, Malaysia 243.09 59,148 243.32 22,863.35
Putrajaya, Malaysia 49.10 6,305 128.41 16,388.23
Petaling Jaya, Selangor, Malaysia 97.95 19,819 202.33 20,876.21
Shah Alam, Selangor, Malaysia 301.42 37,923 125.81 16,179.38
Subang Jaya, Selangor, Malaysia 163.63 25,758 157.41 18,422.92
Klang, Selangor, Malaysia 554.86 38,044 68.57 9,303.66
Ampang Jaya, Selangor, Malaysia 139.37 10,836 77.75 9,155.56
Kajang, Selangor, Malaysia 806.78 38,420 47.62 6,635.30
Selayang, Selangor, Malaysia 523.96 22,224 42.42 6,281.16
Sepang, Selangor, Malaysia 551.71 21,049 38.15 6,421.06
Kuala Langat, Selangor, Malaysia 854.11 19,510 22.84 4,702.28
Kuala Selangor, Selangor, Malaysia 1,195.93 24,995 20.90 3,832.96

#| output: false
gkl_poi_accessibility = calculate_poi_accessibility(gkl_localities, tags)

count    200854.000000
mean          6.310265
std          12.933485
min           0.013223
25%           1.547974
50%           3.702406
75%           7.682236
max        1471.852000
Name: travel_time, dtype: float64
Generating contraction hierarchies with 8 threads.
Setting CH node vector of size 71818
Setting CH edge vector of size 200854
Range graph removed 205530 edges of 401708
. 10% . 20% . 30% . 40% . 50% . 60% . 70% . 80% . 90% . 100%
count    21012.000000
mean        21.877582
std         61.330871
min          0.028598
25%          2.697636
50%          6.613761
75%         16.201957
max       1677.562015
Name: travel_time, dtype: float64
Generating contraction hierarchies with 8 threads.
Setting CH node vector of size 7406
Setting CH edge vector of size 21012
Range graph removed 21828 edges of 42024
. 10% . 20% . 30% . 40% . 50% . 60% . 70% . 80% . 90% . 100%
 100% count    66284.000000
mean        12.097153
std         32.800624
min          0.012130
25%          2.348127
50%          5.106279
75%         10.583654
max       1166.291690
Name: travel_time, dtype: float64
Generating contraction hierarchies with 8 threads.
Setting CH node vector of size 23718
Setting CH edge vector of size 66284
Range graph removed 67782 edges of 132568
. 10% . 20% . 30% . 40% . 50% . 60% . 70% . 80% . 90% . 100%
count    128338.000000
mean         15.473566
std          49.150037
min           0.064617
25%           3.130550
50%           6.491836
75%          13.483481
max        3704.855416
Name: travel_time, dtype: float64
Generating contraction hierarchies with 8 threads.
Setting CH node vector of size 48432
Setting CH edge vector of size 128338
Range graph removed 132204 edges of 256676
. 10% . 20% . 30% . 40% . 50% . 60% . 70% . 80% . 90% . 100%
count    85738.000000
mean        10.949571
std         28.619641
min          0.064644
25%          2.920598
50%          5.776164
75%         10.222416
max       1122.127213
Name: travel_time, dtype: float64
Generating contraction hierarchies with 8 threads.
Setting CH node vector of size 30784
Setting CH edge vector of size 85738
Range graph removed 87702 edges of 171476
. 10% . 20% . 30% . 40% . 50% . 60% . 70% . 80% . 90% . 100%
 100% count    130252.000000
mean          9.338935
std          39.279349
min           0.031060
25%           2.441887
50%           4.423675
75%           8.288388
max        2398.546341
Name: travel_time, dtype: float64
Generating contraction hierarchies with 8 threads.
Setting CH node vector of size 49270
Setting CH edge vector of size 130252
Range graph removed 132396 edges of 260504
. 10% . 20% . 30% . 40% . 50% . 60% . 70% . 80% . 90% . 100%

#| label: tbl-accessibilitystats
#| tbl-cap: Accessibility statistics by Greater KL locality

gkl_poi_accessibility_c = pd.concat(gkl_poi_accessibility, ignore_index=True)
accessibility_score = gkl_poi_accessibility_c.groupby('locality')['Reachable_POIs_Count'].agg(
    mean_reach = 'mean',
    max_reach = 'max',
    min_reach = 'min',
    total_pois = 'count'
).reset_index()
accessibility_score

Locality Mean Reach Max Reach Min Reach Total POIs
Ampang Jaya, Selangor, Malaysia 117.40 183 0 191
Kajang, Selangor, Malaysia 124.23 238 2 520
Klang, Selangor, Malaysia 175.03 254 4 312
Kuala Langat, Selangor, Malaysia 37.46 74 1 185
Kuala Lumpur, Malaysia 259.10 299 0 1,117
Kuala Selangor, Selangor, Malaysia 57.92 90 3 211
Petaling Jaya, Selangor, Malaysia 166.80 261 0 361
Putrajaya, Malaysia 73.89 99 0 112
Selayang, Selangor, Malaysia 62.02 107 0 278
Sepang, Selangor, Malaysia 54.05 93 1 264
Shah Alam, Selangor, Malaysia 156.62 299 1 593
Subang Jaya, Selangor, Malaysia 101.61 160 9 514

import scipy
from scipy.stats import zscore

walkability_score = pd.merge(permeability_score, accessibility_score, on = "locality")

def compute_walkability_index(df):
    """
    Calculate harmonized walkablity scores and index from permeability and accessibilty measure.
    """
    # we use log1p (log(1+x)) to avoid errors if total_pois is 0
    # this rewards a city for having 100 vs 10 POIs more than 1000 vs 910
    df['log_poi_variety'] = np.log1p(df['total_pois'])
    
    # z scaling (The Python 'scale()')
    # We standardize ALL three components so they contribute equally
    # ddof=0 matches R's default population standard deviation calculation
    df['z_intersection'] = zscore(df['intersection_density_km'], nan_policy='omit')
    df['z_reach'] = zscore(df['mean_reach'], nan_policy='omit')
    df['z_variety'] = zscore(df['log_poi_variety'], nan_policy='omit')
    
    # 3. Harmonization with Weighting
    # Logic: 40% Connectivity, 40% Amenity Reach, 20% Total Variety
    df['walkability_index'] = (
        (df['z_intersection'] * 0.4) + 
        (df['z_reach'] * 0.4) + 
        (df['z_variety'] * 0.2)
    )
    
    # 4. Final 0-100 Score for Interpretation
    min_idx = df['walkability_index'].min()
    max_idx = df['walkability_index'].max()
    df['walkability_score'] = 100 * (df['walkability_index'] - min_idx) / (max_idx - min_idx)
    
    return df.sort_values(by='walkability_score', ascending=False)
# calculate harmonized measure of intersection density and mean number of pois accessible out of direction

#| label: tbl-walkabilityscore
#| tbl-cap: Normalised walkability scores and index based on permeablity and accessibility by Greater KL locality

walkability_index = compute_walkability_index(walkability_score)
walkability_index

locality area_m2 intersection_count intersection_density_km street_density_km mean_reach max_reach min_reach total_pois log_poi_variety z_intersection z_reach z_variety walkability_index walkability_score
0 Kuala Lumpur, Malaysia 2.430915e+08 59148 243.315754 22863.352916 259.095792 299 0 1117 7.019297 2.067593 2.305858 2.078190 2.165018 100.000000
2 Petaling Jaya, Selangor, Malaysia 9.795429e+07 19819 202.329056 20876.207334 166.797784 261 0 361 5.891644 1.484581 0.823626 0.192024 0.961688 63.292471
3 Shah Alam, Selangor, Malaysia 3.014190e+08 37923 125.814915 16179.382908 156.615514 299 1 593 6.386879 0.396214 0.660107 1.020378 0.626604 53.070751
4 Subang Jaya, Selangor, Malaysia 1.636344e+08 25758 157.411912 18422.921063 101.607004 160 9 514 6.244167 0.845662 -0.223285 0.781671 0.405285 46.319421
5 Klang, Selangor, Malaysia 5.548601e+08 38044 68.565027 9303.656254 175.025641 254 4 312 5.746203 -0.418132 0.955759 -0.051247 0.204802 40.203687
7 Kajang, Selangor, Malaysia 8.067826e+08 38420 47.621257 6635.298135 124.234615 238 2 520 6.255750 -0.716044 0.140096 0.801045 -0.070170 31.815687
6 Ampang Jaya, Selangor, Malaysia 1.393673e+08 10836 77.751379 9155.561476 117.403141 183 0 191 5.257495 -0.287461 0.030388 -0.868683 -0.276566 25.519597
1 Putrajaya, Malaysia 4.910234e+07 6305 128.405277 16388.227971 73.892857 99 0 112 4.727388 0.433060 -0.668352 -1.755366 -0.445190 20.375725
8 Selayang, Selangor, Malaysia 5.239606e+08 22224 42.415400 6281.163785 62.021583 107 0 278 5.631212 -0.790095 -0.858995 -0.243587 -0.708353 12.347944
9 Sepang, Selangor, Malaysia 5.517062e+08 21049 38.152555 6421.064389 54.053030 93 1 264 5.579730 -0.850731 -0.986964 -0.329698 -0.801018 9.521227
11 Kuala Selangor, Selangor, Malaysia 1.195929e+09 24995 20.900064 3832.956385 57.924171 90 3 211 5.356586 -1.096137 -0.924796 -0.702939 -0.948961 5.008212
10 Kuala Langat, Selangor, Malaysia 8.541135e+08 19510 22.842397 4702.276361 37.459459 74 1 185 5.225747 -1.068509 -1.253443 -0.921788 -1.113138 0.000000

#| label: fig-walkabilityidx
#| fig-cap: Harmonised index of permeability and accessibility

pio.renderers.default = "notebook"

# plot the index
walkablity_plot = px.scatter(walkability_index, x="walkability_index", y="locality")

walkablity_plot

What remains unseen: Quality of walk

The morphological measures are only able to capture the abstract, morphological features of walkability. They can tell the network distance of a place and the theoretical time it takes to walk there navigating the city streets. They remain limited by their assumptions about street networks and make no statements on how humans actually interact with pedestrian environments.

In reality, what makes people walk in the city is influenced by a myriad of factors. It is not only important to ask what makes walking easy, but also what makes walking enjoyable? These are subjective factors of walking often related to the immediate experience of walking in the city, and not captured by morphological measures like permeability or accessibility.

Safety and comfort are two key street attributes that influence subjective walkability [@loukaitou-siderisItSafeWalk12006]. Safety and comfort are sometimes attributes of the physical environment: a steep walk up a hill, potholes and existence of continuous paved walkways can affect people's preference of walking. The design of built environments like dedicated pedestrian paths, the width of these paths, separation of travel modes sharing the same space, the existence of shield from weather elements, et cetera, all contribute to the walkability of an urban space.

Streetscapes are important: compare living streets along a park to major trunk roads filled with bustling automobiles. One is perceived to be more walkable than the other.

The perceptual and social elements are also important: vibrant neighborhoods with many other pedestrians are perceived to be safer, lined shopfronts also served as signifier of safety as people hang out around these spaces [@jacobsDeathLifeGreat1992].

It is beyond the length of this article to cover assessment methods of streetscapes and subjective walkability. However, it remains key that urban design account the subjective elements of walkability with the same importance as the "objective" forms of walkability.

Making streets that work for people

Urban spaces are political, no less its streets. The street is the primary urban public space that is shared among various actors: vehicles, people, road users, shop owners, et cetera. Hence, public spaces can be seen as a kind of commons &mdash; scarce resource that we must share among each other.

Like any commons, the use of this public space is contested. Optimising streets for cars require modifications or design decisions at the expense of other users, like pedestrians. For example, to accommodate car traffic, streets would have to expand multiple car lanes, raising speed limits and braking up connected footpaths. The same can be said about the effect of optimising streets for pedestrians on car mobility. The design of streets is also influenced by the built environment: the buildings that line streets constrain the available functional public space.

What sets streets apart from pastures as commons is that, the manner in which the commons is shared and distributed involves deliberate human interventions and decision-making.  

Past mixture of automotive policy, urban planning and physical development policies were geared towards real estate development and financialization, as well as institutions that prop up car and private property ownership.  In a way, we can see street networks as spaces that reflect its political economy. Their structures and morphology, besides from being shaped by physical limits, are also a function of institutional constraints such as property regimes and political priorities.

Making streets that work for people means reclaiming the social purpose of public spaces that streets represent. Urban mobility that rely on cars can exclude a sizeable share of the city-dwellers that have no means to drive, such as the elderly, children and the differently-abled. By making streets walkable, we also make urban mobility more equitable. Un-walkable streets are also less friendly to tourists and businesses by constraining the foot-access to diverse sociable places. Making streets

### References

::: {#refs}
:::
Read Full Publication

Article highlight

featured report

Conclusion

Attributes
Footnotes
References
Photography Credit

Related to this Publication

No results found for this selection
You can  try another search to see more
No results found for this selection
You can  try another search to see more

Want more stories like these in your inbox?

Stay ahead with KRI, sign up for research updates, events, and more

Thanks for subscribing. Your first KRI newsletter will arrive soon—filled with fresh insights and research you can trust.

Oops! Something went wrong while submitting the form.
Follow Us On Our Socials