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 matplotlibSkill 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:
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 →