Measuring Walkability of Greater Kuala Lumpur

Author

Khoo Wei Yang

Published

December 31, 2025

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 (Lee and Buchner 2008), 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 (Jacobs 1992). 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 (Baobeid, Koç, and Al-Ghamdi 2021). Neighborhoods with higher walkability ratings tend to have higher property values and more vibrant local businesses (Chernobai and Ma 2022).

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 (Tobin et al. 2022).

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.

Figure 1: Source: Jacobs, A. B. (1993)

Figure 1 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 (City of Eugene, n.d.).

Code
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
Code
# select city and crs
cityname = 'kuala lumpur, malaysia'
crs = 32647
Code
# 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)
Code
# 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
)
Code
# 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)

Figure 2 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.

Code
# 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')
Figure 2: A graph representation of Kuala Lumpur

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. Figure 3 shows the intersection per hexagonal area.

Code
# 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')
Figure 3: Intersection counts per hexbin

From Figure 3, 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.

Code
import plotly.express as px
import plotly.subplots as sp
import plotly.io as pio
Code
# 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])
Code
pio.renderers.default = "notebook"
# plot histogram
intersection_dist = px.histogram(int_counts,
                                 labels={"value": "intersections in each bin"})
intersection_dist.update_layout(showlegend=False)
Figure 4

Figure 4 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.

Code
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.

(Moreno 2024)

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 (Abbiasov et al. 2022). 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.

Code
# set up global variables
walk_time = [5, 10, 15] # trip duration in minutes
walk_speed = 4.5 # walking speed in km/hour
Code
# set a uniform walking speed on every edge
for u, v, data in graph.edges(data=True):
    data['speed_kph'] = walk_speed
graph = ox.add_edge_travel_times(graph)
Code
# extract node/edge GeoDataFrames, retaining only necessary columns (for pandana)
nodes_t = ox.graph_to_gdfs(graph, edges=False)[['x', 'y']]
edges_t = ox.graph_to_gdfs(graph, nodes=False).reset_index()[['u', 'v', 'travel_time']]

We define the POIs of our interest as key amenities and sociable third places, examples of which are shown in the code below.

Code
# 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']
}

# Get amentities from place
pois = ox.features.features_from_place(cityname, tags=tags)

# Project pois
pois = pois.to_crs(epsg=crs)
Code
# construct the pandana network model
network = pnda.Network(
    node_x=nodes_t['x'],
    node_y=nodes_t['y'],
    edge_from=edges_t['u'],
    edge_to=edges_t['v'],
    edge_weights=edges_t[['travel_time']]
)

# Extract centroids from the pois' geometries
centroids = pois.centroid
Generating contraction hierarchies with 8 threads.
Setting CH node vector of size 69463
Setting CH edge vector of size 194200
Range graph removed 198912 edges of 388400
. 10% . 20% . 30% . 40% . 50% . 60% . 70% . 80% . 90% . 100%
Code
# Set the pois' locations on the network
for wt in walk_time:
    maxdist = wt * 60  # convert minutes to seconds
    network.set_pois(
        category=f'pois_{wt}min',
        maxdist=maxdist,
        maxitems=10,
        x_col=centroids.x,
        y_col=centroids.y
    )
Code
# calculate travel time to 10 nearest pois from each node in network
for wt in walk_time:
    maxdist = wt * 60
    distances = network.nearest_pois(
        distance=maxdist,
        category=f'pois_{wt}min',
        num_pois=10
    )

# describe summary statistics of walktime to nearest 10 POIs
distances.describe().round(2)
Code
# 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')
Figure 5: Heatmap of walking time to nearest POIs

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.

Figure 5 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 ( Figure 2 ), 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 (UITP 2024). 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 (Chan 2025).

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.

Code
# collect metro station points from osmnx

place = "Selangor"
stations = ox.features_from_place(place, tags = {'railway':['station']})
stations = stations[stations['name'].notnull()]
Code
# ensure all geometries are points
stations["geometry"] = stations.geometry.centroid
Code
stations
Table 1: Collected centroids of train and metro stations in Greater KL
geometry monorail name name:en name:zh network public_transport railway wheelchair name:ms ... name:ar wikimedia_commons old_network addr:suburb internet_access:fee name:zh-Latn-pinyin note name:uk passenger type
element id
node 243306384 POINT (101.62853 3.20871) NaN KA07 Kepong Sentral Kepong Sentral 甲洞中央 NaN station station yes Kepong Sentral ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
1485787800 POINT (101.78779 2.93958) NaN KB07 UKM UKM 马来西亚国立大学 KTM Komuter station station yes UKM ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
3018417115 POINT (101.70566 2.75259) yes KLIA Terminal 1 (Main Building) KLIA Terminal 1 (Main Building) 吉隆坡国际机场1号主航站楼 KLIA Aerotrain station station yes NaN ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
3018417116 POINT (101.7128 2.74743) yes KLIA Terminal 1 (Satellite Building) NaN KLIA卫星航站楼A KLIA Aerotrain station station yes NaN ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
4052254321 POINT (101.63772 3.46853) NaN KA12 Batang Kali Batang Kali 峇冬加里 NaN station station yes Batang Kali ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
way 1069157299 POINT (101.66058 2.98311) NaN PY37 Putra Permai Putra Permai 布特拉柏迈 Putrajaya Line station station NaN Putra Permai ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
1073736705 POINT (101.57999 3.20624) NaN KA08 Sungai Buloh Sungai Buloh 双溪毛糯 KTM ETS station station yes Sungai Buloh ... NaN NaN NaN NaN NaN Shuāngxī Máonuò NaN NaN NaN NaN
1081572645 POINT (101.68138 3.23792) NaN KC05 Batu Caves Batu Caves 黑风洞 KTM Komuter station station yes Batu Caves ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
1084713840 POINT (101.44967 3.04309) NaN KD14 Klang Klang 巴生 KTM Komuter station station yes Klang ... NaN NaN NaN NaN NaN NaN Built in 1890 and it is still serving the town. NaN NaN NaN
1180021471 POINT (101.79053 2.98267) NaN KB06 Kajang Kajang 加影 Seremban Line station station limited Kajang ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN

102 rows × 63 columns

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 Figure 6.

Code
import folium
from shapely.geometry import shape

import os
import time
from dotenv import load_dotenv
import openrouteservice
from openrouteservice import client
Code
load_dotenv()  # reads .env

ORS_API_KEY = os.getenv("ORS_API_KEY")
client = openrouteservice.Client(key=ORS_API_KEY)
Code
# 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"
)
Code
# 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
Code
m
Make this Notebook Trusted to load map: File -> Trust Notebook
Figure 6: Isochrones of walking time to random sample of stations by 5, 10 and 15 minutes

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.

Figure 7 shows the ranking of localities by walkability.

Code
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
Code
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
Code
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']
}
Code
gkl_intersection_stats = get_intersection_density(gkl_localities)
Code
permeability_score = pd.concat(gkl_intersection_stats, ignore_index = True)
permeability_score
Table 2: Permeablity statistics by Greater KL locality
locality area_m2 intersection_count intersection_density_km street_density_km
0 Kuala Lumpur, Malaysia 2.430915e+08 59148 243.315754 22863.352916
1 Putrajaya, Malaysia 4.910234e+07 6305 128.405277 16388.227971
2 Petaling Jaya, Selangor, Malaysia 9.795429e+07 19819 202.329056 20876.207334
3 Shah Alam, Selangor, Malaysia 3.014190e+08 37923 125.814915 16179.382908
4 Subang Jaya, Selangor, Malaysia 1.636344e+08 25758 157.411912 18422.921063
5 Klang, Selangor, Malaysia 5.548601e+08 38044 68.565027 9303.656254
6 Ampang Jaya, Selangor, Malaysia 1.393673e+08 10836 77.751379 9155.561476
7 Kajang, Selangor, Malaysia 8.067826e+08 38420 47.621257 6635.298135
8 Selayang, Selangor, Malaysia 5.239606e+08 22224 42.415400 6281.163785
9 Sepang, Selangor, Malaysia 5.517062e+08 21049 38.152555 6421.064389
10 Kuala Langat, Selangor, Malaysia 8.541135e+08 19510 22.842397 4702.276361
11 Kuala Selangor, Selangor, Malaysia 1.195929e+09 24995 20.900064 3832.956385
Code
gkl_poi_accessibility = calculate_poi_accessibility(gkl_localities, tags)
Code
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
Table 3: Accessibility statistics by Greater KL locality
locality mean_reach max_reach min_reach total_pois
0 Ampang Jaya, Selangor, Malaysia 117.403141 183 0 191
1 Kajang, Selangor, Malaysia 124.234615 238 2 520
2 Klang, Selangor, Malaysia 175.025641 254 4 312
3 Kuala Langat, Selangor, Malaysia 37.459459 74 1 185
4 Kuala Lumpur, Malaysia 259.095792 299 0 1117
5 Kuala Selangor, Selangor, Malaysia 57.924171 90 3 211
6 Petaling Jaya, Selangor, Malaysia 166.797784 261 0 361
7 Putrajaya, Malaysia 73.892857 99 0 112
8 Selayang, Selangor, Malaysia 62.021583 107 0 278
9 Sepang, Selangor, Malaysia 54.053030 93 1 264
10 Shah Alam, Selangor, Malaysia 156.615514 299 1 593
11 Subang Jaya, Selangor, Malaysia 101.607004 160 9 514
Code
import scipy
from scipy.stats import zscore

# now we compute walkability scores for localities
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
    
Code
walkability_index = compute_walkability_index(walkability_score)
walkability_index
Table 4: Normalised walkability scores and index based on permeablity and accessibility by Greater KL locality
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
Code
pio.renderers.default = "notebook"

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

walkablity_plot
Figure 7: Harmonised index of permeability and accessibility

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-Sideris 2006). 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 (Jacobs 1992).

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 — 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 walkable means making cities livable. It is time that we reimagine a form of mobility that is fairer and friendlier for Malaysia.

References

Abbiasov, Timur, Cate Heine, Edward Glaeser, Carlo Ratti, Sadegh Sabouri, Arianna Salazar-Miranda, and Paolo Santi. 2022. “The 15-Minute City Quantified Using Mobility Data.” arXiv. https://doi.org/10.48550/arXiv.2211.14872.
Baobeid, Abdulla, Muammer Koç, and Sami G. Al-Ghamdi. 2021. “Walkability and Its Relationships With Health, Sustainability, and Livability: Elements of Physical Environment and Evaluation Frameworks.” Frontiers in Built Environment 7 (September). https://doi.org/10.3389/fbuil.2021.721218.
Chan, Jade. 2025. “Authorities Urged to Address Parking Woes at Jinjang MRT Station.” The Star, July.
Chernobai, Ekaterina, and Zhongming Ma. 2022. “The Effect of Walkability on House Prices.” Journal of Housing Research 31 (1): 53–73.
Jacobs, Jane. 1992. The Death and Life of Great American Cities. Knopf Doubleday Publishing Group.
Lee, I.-Min, and David M. Buchner. 2008. “The Importance of Walking to Public Health.” Medicine and Science in Sports and Exercise 40 (7 Suppl): S512–518. https://doi.org/10.1249/MSS.0b013e31817c65d0.
Loukaitou-Sideris, Anastasia. 2006. “Is It Safe to Walk?1 Neighborhood Safety and Security Considerations and Their Effects on Walking.” Journal of Planning Literature 20 (3): 219–32. https://doi.org/10.1177/0885412205282770.
Moreno, Carlos. 2024. The 15-Minute City: A Solution to Saving Our Time and Our Planet. John Wiley & Sons.
Tobin, Melissa, Samantha Hajna, Kassia Orychock, Nancy Ross, Megan DeVries, Paul J. Villeneuve, Lawrence D. Frank, et al. 2022. “Rethinking Walkability and Developing a Conceptual Definition of Active Living Environments to Guide Research and Practice.” BMC Public Health 22 (1): 450. https://doi.org/10.1186/s12889-022-12747-3.
UITP. 2024. “Why Walking and Public Transport Must Be Integrated.” UITP.