- Lab
- Data

Build a Choropleth Map in Python to Visualize Covid-19 Case Density by Region
In this Lab, you’ll build choropleth maps in Python to analyze Covid-19 case density across regions. Starting with a pre-computed dataset of cumulative case counts and population by state, you’ll calculate case density, construct a basic choropleth using Folium, and enhance it with customized color scales, legends, tooltips, and pop-ups. You’ll also create an alternative choropleth using Plotly Express to compare library features. By the end of the lab, you will have an interactive, professional-quality map that highlights hotspots and conveys key public health insights.

Path Info
Table of Contents
-
Challenge
Step 1: Build a Basic Choropleth Map with Folium
Step 1: Build a Basic Choropleth Map with Folium
In this step, you'll begin your choropleth map journey by preparing your dataset and rendering your first visualization. You’ll calculate a normalized metric of case density (cases per 100,000 people) and use Folium to map these values by region. You'll ensure your data aligns with the GeoJSON shapes for accurate visualization.
Why this matters Choropleths require a consistent join key between your data and geographic features. Without matching keys and a normalized metric, the map won't render correctly or convey meaningful comparisons.
What You’ll Learn in This Step
- Compute and normalize a metric:
case_density = cases / population * 100000
- Align your dataset’s region codes with GeoJSON keys
- Create a base Folium map and overlay a choropleth layer
Open the Notebook
From the workspace panel, open the notebook file:
1-step-one.ipynb
.info> Important: Save your notebook (Ctrl/Cmd + S) before clicking Validate. Validation inspects the most recent saved checkpoint.
Dataset Properties
Dataset Properties
| Column | Description | | -------------- | ------------------------------------------ | |
region_code
| Integer-based FIPS-style region identifier | |region_name
| Full name of the state (e.g., California) | |cases
| Total case count | |population
| Total population | |case_density
| Precomputed cases per 100,000 people |</details> ## Creating a Comparable Metric and Join Key
Before mapping, you need two key components:
- A metric that adjusts for population size
- A key that matches the GeoJSON file’s
feature.id
values
Metric: Use
case_density = cases / population * 100000
to normalize case counts per 100,000 people. This makes comparisons between regions meaningful.Join key: The
state_code
in your DataFrame must be in string format to match the GeoJSON features. This ensures regions display correctly on the map.Example
df["case_density"] = (df["cases"] / df["population"]) * 100000 df["state_code"] = df["state_code"].astype(str) ``` ## Centering the Map with Folium To build your choropleth, you'll first create a base map using `folium.Map()`. This initializes a map centered at a given location with a set zoom level, acting as the background for your data layers. ### Key Concepts * Use `folium.Map(location=[latitude, longitude], zoom_start=value)` to define where your map starts and how zoomed in it is. * `location` takes a list of two numbers: `[latitude, longitude]` * Assign the result to a variable (like `map_object`) so you can add layers later ### Example
import folium
map_object = folium.Map(location=[00.0, -00.0], zoom_start=5)
This creates a blank interactive map centered at `[0.0, 0.0]` with a medium zoom level. ## Drawing a Basic Choropleth Now that your map is centered, it's time to shade each region based on Covid-19 case density. A **choropleth** visually represents data values by coloring geographic areas. To do this, you'll use `folium.Choropleth()`, which joins your data with a GeoJSON and renders regions using a color scale. ### Key Concepts * `geo_data`: the GeoJSON file containing region shapes * `data`: the DataFrame with the metric you want to visualize * `columns`: two columns — the join key and the metric column * `key_on`: the GeoJSON property to match (typically `"feature.id"`) This task uses **default styling**. You’ll customize the colors and interactivity later. ### Example
folium.Choropleth( geo_data=geojson_object, data=dataframe, columns=["id_column", "value_column"], key_on="feature.id" ).add_to(map_object)
- Compute and normalize a metric:
-
Challenge
Step 2: Customize Color Scheme and Legend
Step 2: Customize Color Scheme and Legend
Now that your map displays data, it's time to make it more readable and insightful. In this step, you'll improve the choropleth by applying a color scale, creating bins for value ranges, and adding a custom HTML legend. These enhancements help viewers understand what the colors represent at a glance.
Why this matters Raw choropleths can be misleading or hard to interpret without clear legends and meaningful bins. Choosing the right color scheme and defining consistent value ranges transforms your map into a powerful communication tool.
What You’ll Learn in This Step
- Apply a custom color palette and value bins to
folium.Choropleth
- Build an HTML-based legend and attach it to the map
- Control how colors reflect case density across regions
Open the Notebook
From the workspace panel, open the notebook file:
2-step-two.ipynb
.info> Important: Save your notebook (Ctrl/Cmd + S) before clicking Validate. Validation inspects the most recent saved checkpoint.
Dataset Properties
Dataset Properties
| Column | Description | | -------------- | ------------------------------------------ | |
region_code
| Integer-based FIPS-style region identifier | |region_name
| Full name of the state (e.g., California) | |cases
| Total case count | |population
| Total population | |case_density
| Precomputed cases per 100,000 people |</details> ## Applying a Custom Color Scale and Bins
Your initial choropleth works, but the default styling may not clearly communicate value differences. You can improve clarity by setting a color palette and value bins using additional parameters in
folium.Choropleth()
.Key Concepts
fill_color
: sets the color scheme (e.g.,"YlOrRd"
,"Blues"
,"Greens"
). These are based on ColorBrewer palettes.threshold_scale
: lets you define value ranges (bins) for coloring. For example,[0, 1000, 2000, 3000]
would group values into 3+ ranges.
When you define bins, each region’s color reflects which range its value falls into.
Example
folium.Choropleth( geo_data=geojson_object, data=dataframe, columns=["id_column", "value_column"], key_on="feature.id", fill_color="Blues", threshold_scale=[0, 10, 20, 30, 40] ).add_to(map_object)
This assigns color shades based on defined value bins. ## Adding a Basic Map Legend
Choropleth maps use color to communicate data, but without a legend, your audience won’t know what those colors mean. Since Folium doesn’t automatically generate legends, we’ll add a simple HTML-based legend manually.
This legend doesn’t need to be styled elaborately, just enough to show what each color represents in terms of case density ranges.
Key Concepts
- Create an HTML
<div>
with labels that match thethreshold_scale
bins. - Use
folium.Element()
to turn the HTML into something you can add to the map. - Use
map_object.get_root().html.add_child(...)
to attach it.
This introduces the concept of custom elements without requiring deep CSS or DOM knowledge.
Example
legend_html = """ <div style="position: fixed; bottom: 30px; left: 30px; background-color: white; padding: 10px; border:1px solid black;"> <strong>Cases per 100k</strong><br> 22000–26000<br> 26000–30000<br> ... </div> """ map_object.get_root().html.add_child(folium.Element(legend_html))
- Apply a custom color palette and value bins to
-
Challenge
Step 3: Add Interactive Tooltips and Pop‑ups
Step 3: Add Interactive Tooltips and Pop-ups
So far, your map shows case density with color — but it doesn’t tell the full story. In this step, you’ll add interactive tooltips and pop-ups so that viewers can explore additional context, like the name of a region, total cases, or population, just by hovering or clicking.
Why this matters
Color alone is rarely enough. Tooltips and pop-ups make your map informative and interactive by revealing details that aren’t obvious from color alone.
What You’ll Learn in This Step
- Add hoverable tooltips to display key stats like region name and case density
- Add clickable pop-ups for detailed metrics like raw case counts and population
- Control the display styling and behavior of interactive elements
Open the Notebook
From the workspace panel, open the notebook file:
3-step-three.ipynb
.info> Important: Save your notebook (Ctrl/Cmd + S) before clicking Validate. Validation inspects the most recent saved checkpoint.
Dataset Properties
Dataset Properties
| Column | Description | | -------------- | ------------------------------------------ | |
region_code
| Integer-based FIPS-style region identifier | |region_name
| Full name of the state (e.g., California) | |cases
| Total case count | |population
| Total population | |case_density
| Precomputed cases per 100,000 people |</details> ## Adding Hover Tooltips with Folium
Tooltips appear when the user hovers over a region on the map. Folium allows you to attach tooltips using
folium.GeoJsonTooltip
, but only for fields that exist within your GeoJSON file — not your DataFrame.In your case, the GeoJSON contains a single property named
"name"
for each region.Key Concepts
folium.GeoJson
: renders a set of geographic shapes from a GeoJSON filetooltip
: used to attach aGeoJsonTooltip
, showing property values from the GeoJSON- You can only reference fields that exist in the
"properties"
of the GeoJSON
Example
folium.GeoJson( data=geojson_object, tooltip=folium.GeoJsonTooltip( fields=["name"], aliases=["Region:"] ) ).add_to(map_object)
Adding Clickable Pop-ups with Region Metrics
You’ve already added hoverable tooltips based on GeoJSON fields. Now you’ll make your map interactive on click — by showing actual Covid-19 metrics from your dataset.
To do this, you’ll inject your DataFrame values into each region’s GeoJSON entry, so they can be accessed by Folium’s pop-up system.
What You’re Doing in This Task
- Match each region in your GeoJSON (
"name"
) to your dataset’sregion_name
- Copy over values from your DataFrame:
cases
,population
,case_density
- Add those values to the
"properties"
section of each region - Display those values with
folium.GeoJsonPopup
📁 Why Do We Need to Inject Data?
folium.GeoJsonPopup()
can only access values stored inside the GeoJSON file — specifically inside the"properties"
section of each feature.Your dataset (
covid_data
) has:cases
population
case_density
But your GeoJSON only includes:
{ "properties": { "name": "Alabama" } }
By injecting data, you expand the
"properties"
section so each region contains all the fields you need.🔍 Matching Keys: GeoJSON vs. DataFrame
You can't use
region_code
to match regions in this case — the GeoJSON uses names like"Alabama"
, and yourregion_code
values are numeric strings like"01"
.Instead, match:
feature["properties"]["name"] == row["region_name"]
This allows you to accurately connect your data with the correct region.
Your map now shows values like case count and population when a region is clicked. But the numbers might look raw or overwhelming — especially large values with no commas or unclear labels.
This task helps you polish the output using simple formatting options built into
folium.GeoJsonPopup
.
Key Concepts
localize=True
adds thousands separators to large numberslabels=True
makes the output look like a labeled key-value tablestyle
allows you to apply inline CSS (like font size or width)
These changes make the pop-up easier to scan and more professional in appearance.
Example
popup = folium.GeoJsonPopup( fields=["cases", "population", "case_density"], aliases=["Total cases:", "Population:", "Case Density:"], localize=True, labels=True, style="font-size: 14px;" )
-
Challenge
Step 4: Style the Map with Custom Colors and Binning
Step 4: Style Your Map with Custom Colors and Binning
Now that your choropleth is interactive, it’s time to style it for clarity and interpretability. In this step, you’ll define custom color bins, apply a color palette, and tune the map’s visual design for real-world readability.
Why this matters Raw data rarely falls into neat buckets. When you control the bin edges and color scheme yourself, you ensure your map tells the story clearly — and avoids misleading visual cues.
What You’ll Learn in This Step
- Create custom data bins based on actual case-density ranges
- Apply a color scale using
fill_color
- Use
threshold_scale
to explicitly control data groupings - Set visual design parameters like opacity and line color
Open the Notebook
From the workspace panel, open the notebook file:
4-step-four.ipynb
.info> Important: Save your notebook (Ctrl/Cmd + S) before clicking Validate. Validation inspects the most recent saved checkpoint.
Dataset Properties
Dataset Properties
| Column | Description | | -------------- | ------------------------------------------ | |
region_code
| Integer-based FIPS-style region identifier | |region_name
| Full name of the state (e.g., California) | |cases
| Total case count | |population
| Total population | |case_density
| Precomputed cases per 100,000 people |</details> ## Creating Custom Bins and Color Scales
You now have a working choropleth, but it still uses automatic binning — which can make your data appear skewed or flatten important patterns. In this task, you'll apply custom bin edges and a color palette that you choose, using values derived from your actual case density range.
This gives you full control over how your data is grouped and represented visually.
Key Concepts
threshold_scale
lets you set manual bins for data classification.fill_color
controls the color palette.- The highest bin should be greater than the maximum value in your data.
- Your data’s join key must match the GeoJSON’s
feature.id
.
Using the `state_code` Field
Your dataset includes a column called
state_code
that contains 2-letter state abbreviations (e.g.,"CA"
,"TX"
).This matches the GeoJSON’s
feature["id"]
, so yourcolumns
definition should now be:columns=["state_code", "case_density"]
Suggested Bin Edges
Use this bin list to create 6 meaningful groupings:
[22000, 26000, 30000, 34000, 38000, 42000, 46000]
These values are based on the actual range in your dataset.
The color gradient bar Folium generates isn’t always easy to interpret — especially when you’ve created your own bins. Instead, you can create a custom HTML-based legend that clearly shows each bin range and ties it to the color scale you used.
This improves readability and makes your map feel more complete and professional.
Key Concepts
- Folium doesn’t automatically create labeled bin legends
- You can manually create a
legend_html
string with HTML - Use
folium.Element(...)
and.get_root().html.add_child(...)
to add the legend to the map - Make sure the bin ranges in the legend match your
threshold_scale
Tip: Positioning and Styling
Your legend can be positioned anywhere using
position: absolute
CSS.Common placements:
- bottom left:
bottom: 10px; left: 10px;
- top right:
top: 10px; right: 10px;
Use
background-color: white;
,padding
, and a border to keep it visible and readable. -
Challenge
Step 5: Finalize the Folium Map and Analyze Densities
Step 5: Finalize the Folium Map and Analyze Densities
You’ve built an interactive, color-coded choropleth map that visualizes Covid-19 case density by region. In this final step, you’ll:
- Add a title directly into the map using a basic HTML element
- Use pandas to compute summary statistics like the average and median case density
- Identify the top and bottom five states based on case rates per 100,000 people
Why this matters A map is more than a visual — it’s a launchpad for data analysis. The final annotations and computed stats will help reinforce what the map is showing and support deeper insight.
What You’ll Learn in This Step
- Use HTML to annotate your Folium map
- Apply pandas methods to compute mean and median
- Extract top and bottom entries from a DataFrame using sorting
Open the Notebook
From the workspace panel, open the notebook file:
5-step-five.ipynb
.info> Important: Save your notebook (Ctrl/Cmd + S) before clicking Validate. Validation inspects the most recent saved checkpoint.
Dataset Properties
Dataset Properties
| Column | Description | | -------------- | ------------------------------------------ | |
region_code
| Integer-based FIPS-style region identifier | |region_name
| Full name of the state (e.g., California) | |cases
| Total case count | |population
| Total population | |case_density
| Precomputed cases per 100,000 people |</details> ## Adding a Title to the Map with HTML
Maps are much more effective when they’re clearly labeled. In this task, you’ll add a title directly to your map using a small HTML snippet.
While Folium doesn’t have a built-in method for titles, you can inject any HTML using
folium.Element
and attach it to the map’s root.
Key Concepts
- The Folium map object supports raw HTML injection
- You can create a string with basic HTML (e.g.,
<h3>...</h3>
) - Use
covid_map.get_root().html.add_child(...)
to attach it - Style the element with CSS if desired — or keep it simple
Example
title_html = "<h3>Covid-19 Case Density by State</h3>" covid_map.get_root().html.add_child(folium.Element(title_html))
This will insert the title into the rendered HTML just above the map. ## Highlighting Groups of Regions Using Statistical Filters
Now that you’ve computed the mean and median case densities, you can use those values to identify groups of regions and create new visual layers on the map.
This lets users explore patterns by toggling overlays like:
- States close to the average case rate
- The exact median state
- Other groupings based on thresholds or rankings (coming next)
How to Filter a DataFrame by Value
You can use conditional logic to return only certain rows from a DataFrame.
Filter for exact values
median_density = df["case_density"].median() subset = df[df["case_density"] == median_density]
This returns rows where
case_density
is exactly equal to the median.Filter for a value range
You can also create a range using greater-than and less-than conditions combined with
&
:mean_density = df["case_density"].mean() subset = df[ (df["case_density"] >= mean_density - 500) & (df["case_density"] <= mean_density + 500) ]
This returns states whose case densities fall within ±500 of the mean.
How to Create a Layer Group for Filtered States
Once you’ve identified the group of states you want to show, use their
state_code
to match against your GeoJSON data.Then, create a
folium.FeatureGroup
, add aGeoJson
layer to it, and use a customstyle_function
that highlights only those states:highlighted = set(filtered_df["state_code"]) folium.FeatureGroup(name="Group Name") folium.GeoJson( geo_data, style_function=lambda feature: { "fillColor": "blue" if feature["id"] in highlighted else "transparent", "color": "blue" if feature["id"] in highlighted else "transparent", "fillOpacity": 0.5 if feature["id"] in highlighted else 0, "weight": 2 } ) ``` ## Find and Display the Top and Bottom 5 States by Case Density To understand which regions are most and least affected, you’ll extract the top and bottom five states based on their case density values. This task uses two useful pandas methods: ### `.nlargest(n, "column")` Returns the top `n` rows sorted by the specified column in **descending** order: ```python df.nlargest(5, "case_density")
This gives you the 5 states with the highest case rates.
.nsmallest(n, "column")
Returns the bottom
n
rows sorted in ascending order:df.nsmallest(5, "case_density")
This gives you the 5 states with the lowest case rates.
Each returns a new DataFrame — so you can inspect, display, or use them in further filtering and mapping.
What's a lab?
Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.
Provided environment for hands-on practice
We will provide the credentials and environment necessary for you to practice right within your browser.
Guided walkthrough
Follow along with the author’s guided walkthrough and build something new in your provided environment!
Did you know?
On average, you retain 75% more of your learning if you get time for practice.