Featured resource
Tech Upskilling Playbook 2025
Tech Upskilling Playbook

Build future-ready tech teams and hit key business milestones with seven proven plays from industry leaders.

Learn more
  • Labs icon Lab
  • Data
Labs

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.

Labs

Path Info

Level
Clock icon Intermediate
Duration
Clock icon 30m
Last updated
Clock icon Aug 18, 2025

Contact sales

By filling out this form and clicking submit, you acknowledge our privacy policy.

Table of Contents

  1. 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:

    1. A metric that adjusts for population size
    2. 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)

  2. 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 the threshold_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))
    
  3. 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 file
    • tooltip: used to attach a GeoJsonTooltip, 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’s region_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 your region_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.

    ## Formatting the Pop-up Display for Clarity

    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 numbers
    • labels=True makes the output look like a labeled key-value table
    • style 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;"
    )
    
  4. 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 your columns 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.

    ## Creating a Custom Legend for Your Bins

    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.

  5. 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 a GeoJson layer to it, and use a custom style_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.