🎯 Introduction & Objectives
In his book The Ghost Map: The Story of London's Most Terrifying Epidemic - and How it Changed Science, Cities and the Modern World, author Steven Berlin Johnson describes how Dr. John Snow, with the assistance of Reverend Henry Whitehead utilized spatial analysis to support their theory that cholera is a waterborne disease. Their analysis resulted in a famous map of the London SOHO neighborhood identifying public water pump locations overlaid with cholera deaths from the August 1854 cholera outbreak.
The intuitive presentation of their spatial analysis convinced local officials and the larger medical community that the Broad Street Pump spread cholera and, more importantly, via contaminated water.
Objective
In this lab, you will recreate one of the first recorded uses of spatial analysis utilizing Geographic Information Systems (GIS). For this lab, you will do the following:
- Recreate the Ghost Map.
- Spatially analyze the 1854 London cholera epidemic.
- Create the maps and a summary of deliverables.
Extract the downloaded zip file to find the following source data:
Ghost_Map.png(Scan of the original map)DEATHS.dbf(Database of households that experienced a cholera fatality)PUMPS.dbf(Database of public water pumps)
🗺️ Option 1: QGIS Analysis
Please note that Esri's ArcMap software has been officially retired. The concepts in the videos remain identical, but we will adapt the exact clicks and buttons to modern QGIS in the steps below!
In 1854, John Snow was tasked with performing and presenting his spatial analysis using a pen and paper. Today, we can use QGIS to recreate and analyze the original Ghost Map.
Step 1: Georeferencing the Original Map
The Ghost_Map.png must be georeferenced before overlaying supporting GIS data. Its native
coordinates are in "model space".
- Open QGIS and start a blank project.
- Before dealing with the map, add your tabular datasets. Go to Layer > Add Layer > Add Vector
Layer...
- Browse for
PUMPS.dbfand add it. Repeat forDEATHS.dbf. Close the dialog.
The adding of the desktop DBF files. - You'll notice nothing is on the map yet because the
.dbffiles only contain data tables, not geometry yet. However,PUMPS.dbfhas coordinates!
This is what it looks like when you open the attribute table of the DBF. - Let's geocode the pumps. Open the Processing Toolbox (Gear icon or Processing >
Toolbox). Search for and run Create points from table.
Adding the pumps via the 'Create points layer from table' tool. - Input table:
PUMPS. X field:X_BNG. Y field:Y_BNG. Target CRS:EPSG:27700 - OSGB36 / British National Grid. Click Run and close. Rename the output layer toPUMPS Events.
Finding the British National Grid (EPSG:27700).
Here are the results after geocoding the points. - Now, select Layer > Georeferencer... from the top menu.
Here is how you find the georeferencer menu. - In the Georeferencer window, click File > Open Raster and select
Ghost_Map.png.
Here is the resulting window after you open the raster. - Click on the Add Point tool. Find a recognizable pump on the PNG (like the Oxford
Market Pump near Market Place/Castle Street East) and click it. Tip: You can turn on labels for
your
PUMPS Eventslayer in the main QGIS window to help identify points.
Turn on labels to help identify which pump is which. - When prompted for coordinates, click the button "From Map Canvas". Your QGIS main
map window will appear. Make sure
PUMPS Eventsis visible and click the corresponding pump point.
Reference the map off the pump. Choose the point, then click 'From Map Canvas'. - Repeat this process for at least 5 different pumps spread across all four corners of the map.
Remember to check the Residual (pixels) column to ensure your points are
geographically accurate.
Here are 5 points selected. Always check the residuals! - Once you have control points, click the yellow gear icon (Transformation Settings).
Choose Polynomial 1 (or Linear) as transformation type, Nearest Neighbour
for resampling, and
set the Target SRS to
EPSG:3857orEPSG:27700. Check the option to Create world file only if you prefer not to create a new modified TIF, though producing a new, modified TIF is also an option. Save the output raster in your folder. Click OK.
Make a world file and run the georeferencer. - Click the green Play button (Start Georeferencing). It's a good idea to save your
GCPs (Ground Control Points) in case you need to troubleshoot later. Close the georeferencer and add
the new georeferenced map to your main QGIS map.
Save your GCPs in case of troubleshooting. - Fixing the map projection: Notice how the map might look squished? This is because
of the default projection (EPSG:4326). If you want the map to look better, you need to set the
project coordinate system to the British National Grid (EPSG:27700) using the button in the bottom
right corner.
A "squished" map happens because of the WGS84 projection.
Changing the coordinate system to EPSG:27700 will make it look correct. - Finally, you can rename layers and rearrange them (by dragging them in the Layers panel) so that the
pumps and deaths show up correctly on top of the map.
Rename and rearrange layers in the Layers panel.
Step 2: Tabular Data Analysis
- We need to bring the deaths onto the map. Run Create points from table again.
- Input:
DEATHS. X field:X_BNG. Y field:Y_BNG. CRS:EPSG:27700. Run and rename toDEATHS Events. - Open the attribute table for
DEATHS Events(Right-click > Open Attribute Table). Note theCOUNTfield (number of fatalities) andSTREETfield. - Open the Processing Toolbox and run Statistics by categories.
- Input layer:
DEATHS Events. Field to calculate:COUNT. Field with categories:STREET. Save the output table. - Open the resulting table and sort by Sum. Which streets have the most fatalities? Is this conclusive evidence on its own? Probably not, but it supports a broader spatial analysis.
Step 3: Proportional Symbols
- Double-click the
DEATHS Eventslayer to open Properties, go to Symbology. - Change the dropdown at the very top of the window that says "Single Symbol" to
Graduated.
The Graduated option is at the top dropdown menu. - Set the Value to
COUNT. - Note that if you use the default Method of Color, it won't do the job effectively. Instead,
set the Method: change it from Color to Size.
As you can see, setting the Method to Graduated Color doesn't really do the job.
You should use Graduated Size instead. - Classes: 3 or 4. Click the Classify button. Play with the settings (such as the
minimum and maximum sizes) until the map reads well.
Click Classify and adjust your settings as needed. - Change the base marker to a red circle (you can click directly on the symbol icon to do this). Click OK.
- View the map. Does displaying fatalities using proportional symbols reinforce or discredit John
Snow's hypothesis? Are there fatalities clustered near other pumps?
Play with the settings until it works well and looks like this.
Step 4: Spatial Statistics
Go to Plugins > Manage and Install Plugins... from the top menu. Search for Standard Deviational Ellipse and click Install Plugin.
Also, make sure the Processing Toolbox is visible by clicking Processing > Toolbox in the top menu.
- In the Processing Toolbox, search for Mean coordinate(s) (usually found under
Vector analysis). Ensure the Input layer is
DEATHS Eventsand Weight field is set toCOUNT. Run it. - How far is the Broad Street Pump from this mean center? (Use the Measure Line tool in QGIS to check).
- In the Processing Toolbox, search for Standard deviational ellipse (sometimes known
as Directional Distribution). Input layer:
DEATHS Events. Weight matrix:COUNT. Standard Deviation:1. Run it. - Are the majority of fatalities clustered around the Broad Street Pump within this 1 standard deviation ellipse?
Step 5: Vector Analysis (Thiessen Polygons & Spatial Joins)
John Snow needed more than visual reinforcement; he needed straight-line least-distance service areas for each water pump.
- In the Processing Toolbox, search for Voronoi polygons (the alternative name for Thiessen Polygons).
- Input layer:
PUMPS Events. Run it. - Overlay the
DEATHS Eventspoints on top of these polygons. Which polygon visually contains the most fatalities? - To quantify this, we'll perform a Spatial Join. In the Processing Toolbox, search for Join attributes by location (summary) (or just use basic Join attributes by location).
- Target layer:
Voronoi polygons. Join layer:DEATHS Events. Summaries to calculate:sum(specifically for the COUNT field). Run it. - Look at the attribute table of the newly generated layer. Which pump really definitively had the most deaths in its immediate service area?
Step 6: Raster Analysis (Kernel Density)
- Turn off the Thiessen polygons and Mean Center layers. Leave the
DEATHS Eventspoints. - Search the Processing Toolbox for Heatmap (Kernel Density Estimation).
- Point layer:
DEATHS Events. Radius:100(in meters). Weight from field:COUNT. Pixel size:2. Run it. - Double-click the heatmap layer, go to Symbology, choose Singleband pseudocolor, pick a color ramp (like YlOrRd), and apply it.
- The Kernel Density analysis creates a smooth surface. Does the resulting surface reinforce or counter Snow's hypothesis?
💻 Option 2: VS Code & Python Scripting (Geopandas & Copilot)
Modern spatial analysis can automate these workflows. Use VS Code, Python, and Copilot to replicate Snow's analysis.
Step 1: Environment Setup
- Open VS Code in your lab folder containing the
.dbffiles. - Install required libraries via terminal:
pip install geopandas libpysal scipy folium simpledbf matplotlib
Step 2: Scripting with Copilot
Create ghost_map.py. We will use simpledbf to read the legacy ArcGIS dbfs, and
geopandas for analysis.
Prompt Copilot (or write) step-by-step:
"""
Goal: Analyze 1854 Cholera Data in Python
1. Read PUMPS.dbf and DEATHS.dbf into pandas DataFrames using Dbf5 from simpledbf.
2. Convert them into GeoDataFrames using X_WGS84 and Y_WGS84. Assign CRS EPSG:4326.
3. Tabular analysis: Group Deaths by 'STREET' and sum 'COUNT'. Print the top 5 streets.
4. Export the data to GeoJSON files or render them onto a Folium map.
"""
Step 3: Reference Complete Script
Here is a working script combining Data Conversion, Voronoi Analysis, and Web Visualization:
import geopandas as gpd
import pandas as pd
from simpledbf import Dbf5
import folium
from folium.plugins import HeatMap
# 1. Load Data
dbf_deaths = Dbf5('DEATHS.dbf').to_dataframe()
dbf_pumps = Dbf5('PUMPS.dbf').to_dataframe()
# 2. To GeoDataFrames
gdf_deaths = gpd.GeoDataFrame(
dbf_deaths,
geometry=gpd.points_from_xy(dbf_deaths.X_WGS84, dbf_deaths.Y_WGS84),
crs="EPSG:4326"
)
gdf_pumps = gpd.GeoDataFrame(
dbf_pumps,
geometry=gpd.points_from_xy(dbf_pumps.X_WGS84, dbf_pumps.Y_WGS84),
crs="EPSG:4326"
)
# 3. Tabular Analysis
top_streets = gdf_deaths.groupby('STREET')['COUNT'].sum().sort_values(ascending=False).head()
print("Streets with most fatalities:\n", top_streets)
# 4. Map Setup
m = folium.Map(location=[51.5133, -0.1366], zoom_start=16, tiles='CartoDB Positron')
# Add Heatmap (Kernel Density equivalent)
heat_data = [[row.geometry.y, row.geometry.x, row['COUNT']] for _, row in gdf_deaths.iterrows()]
HeatMap(heat_data, radius=15).add_to(m)
# Add Proportional Symbols
for _, row in gdf_deaths.iterrows():
folium.CircleMarker(
location=[row.geometry.y, row.geometry.x],
radius=row['COUNT'] * 1.5,
color='red', fill=True, fillOpacity=0.7,
popup=f"{row['COUNT']} deaths"
).add_to(m)
# Add Pumps
for _, row in gdf_pumps.iterrows():
folium.Marker(
location=[row.geometry.y, row.geometry.x],
popup=row['NAME'],
icon=folium.Icon(color='blue', icon='tint')
).add_to(m)
# Save Map
m.save('ghost_map_python.html')
print("\nSuccess! Open ghost_map_python.html in your browser.")
Frame the question → Explore and prepare data → Choose methods and perform analysis → Examine and refine results → Present the analysis
On Friday, September 8, 1854, based on Snow's presentation, the Board of Governors removed the Broad Street pump handle. The resulting knowledge saved countless lives.
Deliverable: With GIS, how would you present your analysis? Create screenshots mapping your vector and raster analyses (or your interactive Python map), and an executive summary documenting your analytical process. Submit these via your course portal.