Data SciencePortfolio projects with Indian public data

Metro city crime rates from NCRB, with honest denominators and ranks

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

Every year, NCRB's Crime in India is followed by headlines ranking cities by crime rate, with Delhi usually near the top. This project rebuilds a city comparison from the source report, then asks two questions most rankings skip. How much does the population denominator affect the result, given that city populations are based on the last census? And how stable is each city's rank once you allow for year-to-year noise in counts? The deliverable is a ranking with intervals and a denominator sensitivity analysis. It is a careful statistical treatment of a dataset people usually misread.

The source

NCRB's Crime in India page lists the latest edition, which covers 2024, and an archive of every edition back to 1953. The page states that data is collected separately for large cities, meaning cities with a population of 10 lakh or more based on the last census.

Download the latest edition and find the city tables through its table of contents. We have not confirmed the chapter number or table titles for the 2024 edition, so check the contents list rather than relying on a number from an older year. Note whether each table is in PDF only or also offered as a spreadsheet, and record which offence heads and which population figure each table uses. Check the table notes for the population year behind any published rate. If the notes do not state it, write that down rather than assuming.

For NCR, check which cities appear. Delhi city will; whether Ghaziabad, Faridabad or others appear depends on the census population threshold, so confirm it in the table itself.

Why employers would care

Safety-tech companies, insurers pricing property or motor risk, urban-policy researchers and newsrooms all use these tables. The statistical point is general and valuable. A rate is only as good as its denominator, and a rank computed from noisy counts can be unstable. Showing a rank interval instead of a single rank is the kind of honesty a data science lead wants to see.

What to watch for in the tables

  • Registered crime, not incidence. Counts reflect cases registered by police. Easier registration, for instance online filing for some offences, can raise a city's count without more crime happening. Look for any cautionary notes in the edition about comparing cities or states, and quote them in your README.
  • The principal-offence rule. Where one incident involves several offences, NCRB's counting methodology records it under the most serious. Check the methodology notes of the edition you use.
  • Fixed census population. If city rates use a census population, fast-growing cities have inflated rates, because their actual population is larger than the denominator.
  • Offence heads. Total cognisable crime mixes very different offences. Compare within heads where possible.

Code: denominators and rank intervals

The cities, counts and populations below are synthetic. The labels are placeholders and none of the numbers are NCRB figures, so the output shows only how the method behaves.

python
import numpy as np
import pandas as pd

rng = np.random.default_rng(3)
cities = pd.DataFrame({
    "city": [f"City {c}" for c in "ABCDEFGH"],
    "cases": [240_000, 52_000, 38_000, 61_000, 29_000, 44_000, 33_000, 18_000],
    "census_pop_lakh": [163, 124, 84, 50, 68, 46, 41, 29],
    "annual_growth": [0.020, 0.012, 0.030, 0.040, 0.010, 0.025, 0.015, 0.035],
})
years_since_census = 13
cities["projected_pop_lakh"] = cities["census_pop_lakh"] * (1 + cities["annual_growth"]) ** years_since_census

cities["rate_census"] = cities["cases"] / cities["census_pop_lakh"]
cities["rate_projected"] = cities["cases"] / cities["projected_pop_lakh"]
cities["rank_census"] = cities["rate_census"].rank(ascending=False).astype(int)
cities["rank_projected"] = cities["rate_projected"].rank(ascending=False).astype(int)

# Rank intervals: treat each count as Poisson with extra year-to-year variation (overdispersion)
sims = 5000
overdispersion = 0.05  # assumed 5% coefficient of variation beyond Poisson
ranks = np.empty((sims, len(cities)), dtype=int)
for s in range(sims):
    lam = cities["cases"] * rng.lognormal(0, overdispersion, len(cities))
    draw = rng.poisson(lam)
    rate = draw / cities["projected_pop_lakh"]
    ranks[s] = pd.Series(rate).rank(ascending=False).to_numpy()
cities["rank_lo"] = np.percentile(ranks, 2.5, axis=0).astype(int)
cities["rank_hi"] = np.percentile(ranks, 97.5, axis=0).astype(int)

cols = ["city", "rate_census", "rank_census", "rate_projected", "rank_projected", "rank_lo", "rank_hi"]
print(cities[cols].round(0).to_string(index=False))

Pure Poisson noise is tiny for counts in the tens of thousands, so the simulation adds an overdispersion term. In a real analysis, estimate that term from the city's own year-to-year variation over several editions rather than assuming 5%. The denominator switch alone can move cities several places, which is usually the larger effect.

Analysis plan

  1. 1Extract city-level counts for total cognisable crime and three or four specific heads from the latest edition, plus the population used.
  2. 2Build a projected-population denominator from an official projection if one exists at city level. If it does not, run a sensitivity range of growth rates and label it as such.
  3. 3Collect the same tables from the five previous editions. Estimate each city's overdispersion from the residual year-to-year variation after a simple trend.
  4. 4Produce rank intervals for each head.
  5. 5Write up which conclusions survive both the denominator change and the noise, and which do not.

Deliverables

  • Interval chart: each city's rank with its interval, census and projected denominators side by side.
  • Slope chart: rank under the census denominator against rank under the projected one.
  • Table: city, cases, population used, rate, projected rate, rank interval, by offence head.
  • README: edition and table references, population sources and years, growth assumptions, overdispersion method, any cautionary notes from the edition quoted.

Interview questions

  • Why is a registered-crime rate not a measure of crime incidence?
  • How does a fixed census denominator bias rates for fast-growing cities?
  • Why add overdispersion instead of relying on Poisson noise?
  • Two cities' rank intervals overlap. What can you say about which is "safer"?
  • How would you model the effect of an online FIR system on registered counts?

For another NCR denominator problem, see the Delhi tele-density project. Simulation-based uncertainty is part of our Data Science programme.

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.