Skip to content
How-to10 min read

How to Create a Heatmap from Location Data (Without Misleading Anyone)

Bandwidth, normalisation and colour classification each change the conclusion. A heatmap without those three stated is decoration.

Author
HuiTu Technology
Published

A heatmap looks objective. It is a picture of where the data is dense, and pictures feel like facts. In reality the same points can produce one broad hotspot or five distinct ones depending on choices the analyst made, and both maps look equally authoritative.

This guide covers the five decisions that determine what a heatmap actually says, and how to make each one defensible.

1. Clean the points first

Density maps amplify data errors rather than hiding them. The specific error that ruins heatmaps is centroid fallback: when geocoding cannot find an address, many services return the centre of the postcode or the city. Those records pile up on one coordinate and produce a hotspot that is purely an artefact.

-- Coordinates shared by many records are almost always centroid fallbacks
SELECT ROUND(lat::numeric, 5) AS lat,
       ROUND(lon::numeric, 5) AS lon,
       COUNT(*) AS records
FROM points
GROUP BY 1, 2
HAVING COUNT(*) > 5
ORDER BY records DESC;

2. Project into an equal-area system

Never compute density in degrees. A degree of longitude shrinks as you move away from the equator, so a fixed-radius kernel in degrees covers less ground in the north of your study area than in the south. Reproject to a UTM zone, a national grid or an equal-area projection before any distance calculation.

3. Choose the aggregation method

Three methods, three different jobs
MethodOutputUse when
Kernel density estimationA smooth continuous rasterYou want to see the shape of a phenomenon
Hexagonal binningEqual-area polygons with countsYou need to join, compare or report by unit
Square gridGrid cells with countsThe result must align with an existing raster or reporting grid

Hexagons are preferred over squares for binning because every neighbour is equidistant, so hexbins do not introduce the directional artefacts that a square grid produces along its axes.

4. Choose and test the bandwidth

Bandwidth is the radius of influence of each point, and it is the single most consequential parameter. Too small and you see individual points; too large and everything merges into one blob centred on the busiest area.

  • Start from the scale of the decision. A walkable retail question suggests 200 to 400 m. A regional service question suggests 2 to 5 km.
  • Produce the surface at three or four bandwidths, not one.
  • Check which hotspots persist across all of them. Those are real features. Ones that appear at a single bandwidth are artefacts of that choice.
  • Publish the bandwidth on the map. A density legend without units or radius is not interpretable.

5. Normalise by the right denominator

This is the step most often skipped, and it is usually the one that changes the answer. Raw counts of almost any human activity produce a hotspot over the city centre, because that is where the people are. The map has rediscovered population density, at some expense.

Choosing a denominator
QuestionNormalise by
Where do residents generate the most activity?Residential population
Where is daytime activity concentrated?Workplace population
Where is a category over-supplied?Competing venues or floorspace
Where is the risk highest per exposure?Trips, visits or hours of exposure
Where is absolute volume greatest?Nothing; raw counts are correct here

Producing both the raw and the normalised surface is usually right. The comparison between them is often the most informative output of the whole exercise.

Density normalised per resident, 400 m bandwidthLowHigh

Then test whether the hotspots are real

A heatmap shows where values are high. It does not tell you whether that concentration exceeds what random distribution would produce. Getis-Ord Gi* does, returning a z-score and a confidence level per cell, so a hotspot becomes a claim you can defend rather than a colour you chose.

import geopandas as gpd
from libpysal.weights import Queen
from esda.getisord import G_Local

hexes = gpd.read_file("hexbins.gpkg")
weights = Queen.from_dataframe(hexes)
weights.transform = "r"

gi = G_Local(hexes["count_per_1k"], weights, permutations=999)
hexes["z"] = gi.Zs
hexes["p"] = gi.p_sim
hexes["hotspot"] = (hexes.z > 1.96) & (hexes.p < 0.05)

Finally, choose the colours honestly

  • Use a perceptually uniform ramp such as viridis or magma. Rainbow ramps invent boundaries that are not in the data.
  • State the classification method. Quantiles, equal interval and natural breaks produce very different maps from identical data.
  • Label the legend with real units, not 'low' to 'high'.
  • Check the ramp for colour-vision deficiency before publishing.

More from the blog

Keep reading

Next step

Need this done rather than explained?

If the article describes a problem you are facing, tell us the specifics and we will scope it with a fixed price.