Skip to content

Python: GeoPandas & Folium for Maps

Automate your spatial analysis — read shapefiles, join data, and create interactive HTML maps, all from Python.


Libraries
geopandas, folium, matplotlib, plotly
Install
pip install geopandas folium matplotlib
Skill Level
🔵 Basic Python required
Output
Static PNG maps + Interactive HTML maps
Best For
Automated analysis pipelines, reproducible maps
Documentation

Installation

# Activate your virtual environment first
# Windows: bookenv\Scripts\activate

# Install (conda is easier on Windows for geopandas)
conda install -c conda-forge geopandas folium matplotlib seaborn plotly

# OR with pip (may need extra steps on Windows)
pip install geopandas folium matplotlib seaborn

Windows Installation Tip

On Windows, pip install geopandas sometimes fails due to GDAL dependencies. Use conda via Miniconda for the simplest experience:

conda create -n bookenv python=3.11
conda activate bookenv
conda install -c conda-forge geopandas folium jupyter


Part 1: Reading Spatial Data with GeoPandas

import geopandas as gpd
import pandas as pd
import matplotlib.pyplot as plt

# Read a Shapefile (same as opening in QGIS, but in Python)
india_districts = gpd.read_file("gadm41_IND_2.json")

print(f"Type: {type(india_districts)}")  # GeoDataFrame
print(f"Rows: {len(india_districts)}")   # ~748 districts
print(f"CRS: {india_districts.crs}")     # EPSG:4326

# Plot basic map (all districts, no data)
fig, ax = plt.subplots(figsize=(12, 10))
india_districts.plot(ax=ax, edgecolor='gray', linewidth=0.3, color='#f5f5f5')
ax.set_title("India — District Boundaries (GADM Level 2)")
ax.axis('off')
plt.tight_layout()
plt.savefig("india_districts.png", dpi=150, bbox_inches='tight')
plt.show()

Part 2: Joining Census Data and Making a Choropleth

import geopandas as gpd
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.colors import BoundaryNorm
import matplotlib.cm as cm

# Load district boundaries
gdf = gpd.read_file("gadm41_IND_2.json")

# Load Census literacy data (downloaded from data.gov.in)
census = pd.read_csv("census_district_literacy_2011.csv")

# Standardize district names (remove trailing spaces, uppercase)
gdf['NAME_2_clean'] = gdf['NAME_2'].str.strip().str.upper()
census['District_clean'] = census['District'].str.strip().str.upper()

# Join
merged = gdf.merge(census, left_on='NAME_2_clean', right_on='District_clean', how='left')
print(f"Joined: {merged['Literacy_Rate'].notna().sum()} of {len(merged)} districts have data")

# Plot Choropleth
fig, ax = plt.subplots(1, 1, figsize=(14, 12))

merged.plot(
    column='Literacy_Rate',
    ax=ax,
    legend=True,
    cmap='RdYlGn',
    scheme='quantiles',
    k=7,
    missing_kwds={'color': 'lightgrey', 'label': 'No data'},
    legend_kwds={
        'title': 'Literacy Rate (%)',
        'loc': 'lower right',
        'fontsize': 9
    },
    edgecolor='white',
    linewidth=0.2
)

ax.set_title("Literacy Rate by District — India (Census 2011)", fontsize=16, pad=15)
ax.axis('off')

# Add source note
ax.text(0.01, 0.01, "Source: Census of India 2011 | Map: GADM 4.1",
        transform=ax.transAxes, fontsize=8, color='gray',
        verticalalignment='bottom')

plt.tight_layout()
plt.savefig("india_literacy_choropleth.png", dpi=200, bbox_inches='tight')
plt.show()

Part 3: Interactive Map with Folium

import folium
import geopandas as gpd
import pandas as pd
import json

# Load data
gdf = gpd.read_file("gadm41_IND_2.json")
census = pd.read_csv("census_district_literacy_2011.csv")
merged = gdf.merge(census, left_on='NAME_2', right_on='District', how='left')

# Convert to WGS84 GeoJSON for Folium
merged_4326 = merged.to_crs(epsg=4326)

# Create Folium map
m = folium.Map(location=[22, 80], zoom_start=5,
               tiles='CartoDB positron')

# Add Choropleth layer
folium.Choropleth(
    geo_data=merged_4326.__geo_interface__,
    data=merged[['District', 'Literacy_Rate']],
    columns=['District', 'Literacy_Rate'],
    key_on='feature.properties.NAME_2',
    fill_color='RdYlGn',
    fill_opacity=0.7,
    line_opacity=0.2,
    legend_name='Literacy Rate (%)',
    nan_fill_color='lightgrey',
    nan_fill_opacity=0.4,
    name='Literacy Rate'
).add_to(m)

# Add hover tooltips
style_function = lambda x: {'fillColor': '#ffffff', 'color': '#000000',
                              'fillOpacity': 0.1, 'weight': 0.1}
highlight_function = lambda x: {'fillColor': '#000000', 'color': '#000000',
                                  'fillOpacity': 0.50, 'weight': 0.1}

NIL = folium.features.GeoJson(
    data=merged_4326.__geo_interface__,
    style_function=style_function,
    highlight_function=highlight_function,
    tooltip=folium.features.GeoJsonTooltip(
        fields=['NAME_2', 'NAME_1', 'Literacy_Rate'],
        aliases=['District:', 'State:', 'Literacy Rate (%)'],
        style=("background-color: white; color: #333333; font-size: 13px; padding: 6px;")
    )
)
m.add_child(NIL)

# Layer control
folium.LayerControl().add_to(m)

# Save as interactive HTML
m.save("india_literacy_interactive.html")
print("Saved to india_literacy_interactive.html")
print("Open this file in any browser to see the interactive map!")

✏️ Practice Exercise

Exercise 7.2 — Interactive Folium Map of Your State

Goal: Create an interactive web map of your state showing district-level data.

Use any dataset from this book — Census literacy, NFHS stunting, MGNREGA wages...

Template:

import folium
import geopandas as gpd
import pandas as pd

# Load state boundaries
gdf = gpd.read_file("gadm41_IND_2.json")
state_gdf = gdf[gdf['NAME_1'] == 'Maharashtra']  # Change state

# Load your data
data = pd.read_csv("your_data.csv")  # Must have a District column

# Join
merged = state_gdf.merge(data, left_on='NAME_2', right_on='District', how='left')

# Map centred on your state
state_center = [merged.geometry.centroid.y.mean(),
                merged.geometry.centroid.x.mean()]
m = folium.Map(location=state_center, zoom_start=7)

folium.Choropleth(
    geo_data=merged.to_crs(4326).__geo_interface__,
    data=merged[['District', 'Your_Column']],  # Change column name
    columns=['District', 'Your_Column'],
    key_on='feature.properties.NAME_2',
    fill_color='Blues',
    legend_name='Your Indicator'
).add_to(m)

m.save("my_state_map.html")

  • Which districts in your state stand out as highest/lowest?
  • Share the HTML file — anyone can open it in a browser, no software needed!

Next: MapLibre for Web Maps →