Clustering Map Markers with Folium

Dropping ten thousand individual folium.Marker objects onto a map freezes the browser — every marker is a DOM node, and Leaflet renders them all at once. This guide replaces raw markers with server-aware clustering so a dense point set stays interactive. It is aimed at anyone plotting incident reports, sensor networks, or store locations, and sits under Interactive Maps with Folium within Web Mapping & Interactive Visualization.

Why This Approach / What Goes Wrong

A folium.Marker becomes a real DOM element with an icon image and event handlers. Browsers handle a few hundred comfortably; a few thousand introduce lag, and tens of thousands hang the page on load. MarkerCluster (from the Leaflet.markercluster plugin Folium bundles) groups nearby points into a single count badge that splits apart as you zoom in, so only the visible, de-densified set renders. For very large point sets, you also want FastMarkerCluster, which skips per-marker Python objects entirely and passes raw coordinate arrays to JavaScript.

The naive alternative — plotting everything and hoping — produces a map that technically loads but is unusable, and an HTML file bloated with one <div> per point.

Prerequisites

conda install -c conda-forge "geopandas=0.14.*" "folium=0.16.*"

Step-by-Step Implementation

1. Load points and reproject to WGS84.

import geopandas as gpd

# incidents: ~40k point features, originally in EPSG:2154 (Lambert-93)
incidents = gpd.read_file("incidents.gpkg").to_crs(epsg=4326)
incidents = incidents[incidents.geometry.notnull() & ~incidents.geometry.is_empty]

2. Extract a plain [lat, lon] array. Folium wants latitude first.

coords = [[geom.y, geom.x] for geom in incidents.geometry]

3. For moderate counts, use MarkerCluster with real markers (keeps per-point popups).

import folium
from folium.plugins import MarkerCluster

center = incidents.geometry.union_all().centroid
incident_map = folium.Map(location=[center.y, center.x], zoom_start=11, tiles="CartoDB positron")

cluster = MarkerCluster(name="Incidents").add_to(incident_map)
for (lat, lon), label in zip(coords, incidents["category"]):
    folium.Marker([lat, lon], tooltip=label).add_to(cluster)

folium.LayerControl().add_to(incident_map)
incident_map.save("incidents_clustered.html")

4. For large counts (>20k), use FastMarkerCluster — it serializes the array directly, no Python Marker objects.

import folium
from folium.plugins import FastMarkerCluster

fast_map = folium.Map(location=[center.y, center.x], zoom_start=11, tiles="CartoDB positron")
FastMarkerCluster(data=coords, name="Incidents").add_to(fast_map)
folium.LayerControl().add_to(fast_map)
fast_map.save("incidents_fast.html")

Verification

Check the point count survived the pipeline and the output stays light.

print(f"Plotted {len(coords)} points")     # Plotted 39812 points
assert len(coords) == len(incidents)

import os
size_mb = os.path.getsize("incidents_fast.html") / 1e6
print(f"FastMarkerCluster output: {size_mb:.2f} MB")  # FastMarkerCluster output: 2.31 MB

FastMarkerCluster output should be a fraction of the equivalent MarkerCluster file because it stores a compact coordinate array, not thousands of marker definitions.

Edge Cases & Debugging