Data SciencePortfolio projects with Indian public data

Delhi Metro as a network graph, built from the DMRC GTFS feed

PK
Pankit Kumar
Sr. Data Scientist at Parexel (a Goldman Sachs–backed company) · 27 September 2026 · 5 min read
In this article (7 sections)

Related programme: the Data Science course (6 months; ₹1,30,000 (inclusive of GST) · EMI from ₹5,000/month).

The short answer

Which Delhi Metro stations hold the network together, and how much longer do journeys get when one of them closes? A timetable feed can answer this once you convert it into a graph: stations are nodes, and consecutive stops on a trip become edges. This project builds that graph from the metro feed on Delhi's Open Transit Data portal. It then resolves the step most tutorials skip, merging interchange stations that appear under several stop IDs. After that it computes shortest paths, a betweenness-style centrality and a closure test. It is a compact graph-analytics project on familiar local data, and you can explain every number.

Source and its limits

The DMRC Static Data page on Open Transit Data offers the metro feed as one zip after a short name-and-purpose form. When we checked on 27 September 2026, the page listed 262 stations, 36 routes, 17,997 trips and 4,11,686 stop times, all "Last Updated: Aug. 10, 2023". The files follow the GTFS standard, so expect stops.txt, routes.txt, trips.txt and stop_times.txt with the usual column names. Check the downloaded file's header for anything optional.

Three limits shape the project:

  • Age. Any line or extension opened after August 2023 will be missing. State that clearly and do not describe the output as today's network.
  • Estimated times. The portal's documentation warns that bus stop times are a rough constant-speed estimate. Treat metro times with the same caution, and use hop counts as the primary distance.
  • Route count. Thirty-six routes for a network with far fewer lines suggests that directions or branches are separate routes. Inspect routes.txt and trips.txt (direction_id, if present) before counting lines.

Why it matters to employers

Graph thinking sits behind supply-chain networks, fraud rings, telecom topology and recommendation systems. A metro network is a small, intuitive case. Interviewers can follow your reasoning, and you can show the usual difficulties: entity resolution for merging stops, a choice of edge weights, and the difference between a structural measure and a claim about passengers. The feed has no ridership, so this project says nothing about crowding.

Building the graph

  1. 1From stop_times, sort by trip_id and stop_sequence and pair each stop with the next one. Deduplicate pairs across trips to get the edge list.
  2. 2Merge interchanges. An interchange may carry one stop_id per line. Group stops by normalised name and, as a check, by distance under about 300 metres. List every merge in a table so a reviewer can reject one.
  3. 3Make the graph undirected for structural analysis, and attach the set of routes serving each node.
  4. 4Compute the hops between every pair with breadth-first search. At a few hundred nodes, all-pairs BFS runs in seconds.

Code on a synthetic network

The network below is synthetic: two small invented lines that share one interchange, which is listed under two IDs, as can happen in real feeds. It uses pandas and the standard library only.

python
from collections import deque
import pandas as pd

st = pd.DataFrame({
    "trip_id": ["A1"] * 5 + ["B1"] * 4,
    "stop_sequence": [1, 2, 3, 4, 5, 1, 2, 3, 4],
    "stop_id": ["a1", "a2", "x_A", "a4", "a5", "b1", "x_B", "b3", "b4"],
})
stops = pd.DataFrame({
    "stop_id": ["a1", "a2", "x_A", "a4", "a5", "b1", "x_B", "b3", "b4"],
    "stop_name": ["Alpha", "Birch", "Central Jn", "Delta", "Echo", "Fern", "Central Jn.", "Grove", "Hill"],
})
route_of = {"A1": "Line A", "B1": "Line B"}

# Entity resolution: normalise names so both interchange IDs map to one node
stops["node"] = stops["stop_name"].str.lower().str.replace(r"[^a-z ]", "", regex=True).str.strip()
merges = stops.groupby("node")["stop_id"].agg(list)
print("merged nodes:", merges[merges.str.len() > 1].to_dict())
st = st.merge(stops[["stop_id", "node"]], on="stop_id").sort_values(["trip_id", "stop_sequence"])

st["next"] = st.groupby("trip_id")["node"].shift(-1)
edges = st.dropna(subset=["next"])[["node", "next"]].drop_duplicates()
adj = {}
for u, v in edges.itertuples(index=False):
    adj.setdefault(u, set()).add(v)
    adj.setdefault(v, set()).add(u)
lines = st.assign(line=st["trip_id"].map(route_of)).groupby("node")["line"].agg(lambda s: sorted(set(s)))

def bfs(src, closed=frozenset()):
    dist, q = {src: 0}, deque([src])
    while q:
        u = q.popleft()
        for w in adj[u] - closed:
            if w not in dist:
                dist[w] = dist[u] + 1
                q.append(w)
    return dist

nodes = sorted(adj)
through = dict.fromkeys(nodes, 0)
total_hops, pairs = 0, 0
for s in nodes:
    d = bfs(s)
    for t in nodes:
        if t <= s:
            continue
        total_hops += d[t]; pairs += 1
        for m in nodes:                     # m lies on a shortest s-t path
            if m not in (s, t) and d.get(m, 99) + bfs(m).get(t, 99) == d[t]:
                through[m] += 1

summary = pd.DataFrame({"lines": lines, "degree": {n: len(adj[n]) for n in nodes}, "pairs_through": through})
print(summary.sort_values("pairs_through", ascending=False).to_string())
print("mean hops:", round(total_hops / pairs, 2))

closed = {"central jn"}
d_birch = bfs("birch", frozenset(closed))
print("reachable from Birch with the interchange closed:", sorted(d_birch))

Without the name merge, the two lines would be separate components and every cross-line journey would look impossible. After the merge, the interchange lies on the most shortest paths. Closing it disconnects the two lines entirely, the simplest version of the resilience result you will compute on the real graph. On a real network, the inner loop that recomputes BFS is slow. Store every source's BFS result once, or use Brandes' algorithm for exact betweenness.

Analysis plan

  • Reproduce the page's counts from the zip, and inspect how routes map to lines and directions.
  • Build the merged station graph and publish the merge table.
  • Compute degree, all-pairs hops and betweenness. List the top fifteen stations and the lines each serves.
  • For the top five, remove the station and report the change in mean hops and the number of station pairs disconnected.
  • As a sensitivity check, weight edges by scheduled time and see whether the ranking changes. Remember that the times are estimates.

Deliverables

  • Network map: stations plotted by coordinates, sized by betweenness, interchanges highlighted.
  • Closure table: station, lines, pairs disconnected, change in mean hops.
  • Merge table: original stop IDs, names, distance apart, merged node.
  • README: feed date and counts, route-to-line mapping, merge rules, why hops were preferred to times, the absence of ridership.

Interview questions

  • Why did you use hop count instead of scheduled travel time?
  • Two stations have the same name but are 900 metres apart. Merge or not?
  • What does high betweenness mean here, and why is it not the same as "most crowded"?
  • How would your closure results change if you added walking transfers between nearby stations?

Graph methods and simulation both feature in our Data Science programme. If you want a gentler first step with the same portal, start with the bus timetable audit.

PK
Pankit Kumar
Lead Instructor, NeuraPath Academy

Pankit Kumar has 10 years in Data Science & AI, building and shipping production systems in regulated pharma and clinical environments. He is a freelance trainer at Boston Institute of Analytics, AnalytixLabs and Scaler, and has taught this material to thousands of working professionals.

This article is part of our Data Science programme — 6 months. From data foundations to machine learning, deep learning and deployment.

Explore Data Science
Counselling is free · no obligation

Not sure which programme fits?

Tell us your background and we will map it to the right entry point — including saying so when a cheaper programme is the better fit. A counsellor replies within one working day.