Module 2: Collections, Loops, and Functions

PUBHLT 0411 · Python for Public Health Data Analysis

Soumik Purkayastha

Dept. of Biostatistics and Health Data Science, University of Pittsburgh

October 15, 2026

Forty-nine months.

We will examine 49 months of COVID-19 death data across all 67 counties in Pennsylvania.

Pennsylvania recorded COVID-19 deaths across 49 months and 67 counties

That is \(49 \times 67 = 3,283\) county-months of data.

The question this session opens

Identifying the deadliest month requires examining every month and retaining the largest. Performed by hand, this is 49 comparisons for the state and 3,283 for the counties.

This session establishes the machinery that performs the comparison. A collection to hold the values, a loop to walk them, and a function to package the work for reuse.

Required from Module 1: if / elif / else, indentation, lists, .append(), +=.

Two views of the same 3,283 county-months

Each cell of the heatmap and each point on a grey line is one county-month. Producing either figure requires walking all 3,283 of them.

A dictionary maps keys to values

A list is indexed by position. A dictionary is indexed by a key chosen by the author.

Each entry is a key: value pair. Keys are unique; assigning to an existing key replaces its value.

R analogue

A named list, or a two-column lookup table.

A missing key raises KeyError

KeyError — the key is not present

KeyError: '2022-06'

Subscripting a dictionary with an absent key raises immediately rather than returning an empty value. The exception names the offending key.

Resolution: .get(key, default) returns the default instead of raising — covid_deaths.get("2022-06", 0) evaluates to 0.

for walks every element of a collection

Iterating a dictionary yields its keys. The colon and the indented block follow the same rule as if.

.items() yields both parts at once, which is the idiomatic form.

Three views of a dictionary

.keys() yields the labels, .values() yields the quantities, .items() yields both.

range() generates a sequence of integers

1. range(n) yields 0 through n - 1 — n values, starting at zero

2. range(a, b) yields a through b - 1 — the stop is excluded, as in slicing

range() excludes its endpoint

range(2020, 2024) yields 2020, 2021, 2022, and 2023. Reaching 2024 requires range(2020, 2025).

The accumulator pattern builds a result across iterations

Initialize a variable before the loop; update it inside.

Initialization belongs outside the loop

A counter initialized inside the loop resets on every iteration and finishes holding only the final value.

The maximum requires remembering the best so far

The loop retains a candidate and replaces it whenever a larger value appears. This is the same procedure a person performs by hand, stated precisely.

Checkpoint: the comparison is inside the wrong block

Objective: count the months exceeding 2,000 deaths.

Indentation determines what runs when

The final print() is outside the loop and reports the total once, correctly. The running count is inside the if and prints on every match.

Moving the final line inside the loop would report a partial count as though it were the answer — with no error raised.

while repeats until a condition fails

A for loop runs a known number of times. A while loop runs until something changes.

A condition that never becomes false runs forever

Every while loop requires something inside it that moves the condition toward failure. Omitting position += 1 produces a loop that never terminates.

while True: with break is an exit-controlled loop

break exits the loop immediately. continue skips to the next iteration without leaving.

The loop that tests at the bottom

while True: with break runs the body first and decides afterward. This is Python’s equivalent of a do-while loop, and it suits searches that stop at the first match.

Functions.

The same calculation, written once and reused — instead of copied and edited every time a new county arrives.

def packages a computation under a name

1. def names the function; the parameters are named in parentheses

2. The docstring — a string on the first line — states what the function returns

3. return sends a value back to the caller

A function without return returns None

Printing a value inside a function displays it; it does not hand it back. Only return produces a value the caller can store or reuse.

A function may return more than one value

Returning several values produces a tuple, and assigning to several names unpacks it.

A tuple is an immutable sequence

Tuples index and slice exactly as lists do. They cannot be modified after creation — there is no .append().

Tuples signal fixed structure

A list holds a variable number of similar things. A tuple holds a fixed number of related things — a record whose shape is known.

sorted() orders a collection, and key= chooses by what

key= receives a function returning the value to sort on. lambda writes that function inline.

Checkpoint: ranking by count answers a different question

Counts rank by population; rates rank by burden

The two orderings answer different questions. Ranking Pennsylvania’s 67 counties by COVID death count returns the five largest counties. Ranking by rate returns five rural counties, and the two lists share no members at all.

A county appears at the top of a count ranking largely because many people live there.

Objects.

What the dot in .items(), .append(), and .strip() means.

Every value in Python is an object

An object carries data and the operations defined for it. The dot accesses both.

"text".upper(), counties.append(), and covid_deaths.items() are the same construction: a value, a dot, and an operation that value supports.

Methods belong to types

.upper() exists on strings and not on lists. Calling a method a type does not define raises AttributeError, which names both the type and the missing attribute.

A class defines a new kind of object

1. __init__ runs when the object is created and stores its data

2. self refers to the object being operated on

3. An attribute holds data; a method performs an operation

Attributes and methods differ by the parentheses

schuylkill.name retrieves stored data. schuylkill.rate_per_100k() calls a function. Omitting the parentheses returns the function itself rather than its result.

The dot is the same idea everywhere

What Module 4 introduces

pandas supplies an object holding an entire table. df.head() and df.groupby() use the construction on this slide — a value, a dot, an operation.

The worst month.

Putting the session together to answer the question it opened with - which month was the worst?

Exercise: find the deadliest month

# The blank is the method yielding both the key and the value: items
# Iterating the dictionary directly would give only the month, and `deaths`
# would be undefined.
covid_deaths = { "2021-08": 555, "2021-09": 1248, "2021-10": 2035, "2021-11": 2117, "2021-12": 3365, "2022-01": 4294, "2022-02": 1819, } worst_month = "" worst_deaths = 0 for month, deaths in covid_deaths.items(): if deaths > worst_deaths: worst_deaths = deaths worst_month = month print(f"Worst month: {worst_month} with {worst_deaths:,} deaths")
covid_deaths = {
    "2021-08": 555, "2021-09": 1248, "2021-10": 2035, "2021-11": 2117,
    "2021-12": 3365, "2022-01": 4294, "2022-02": 1819,
}

worst_month = ""
worst_deaths = 0

for month, deaths in covid_deaths.items():
    if deaths > worst_deaths:
        worst_deaths = deaths
        worst_month = month

print(f"Worst month: {worst_month} with {worst_deaths:,} deaths")

January 2022

Figure 1. Statewide COVID-19 deaths for each of the 49 months in the file. The series rises through late 2021 to a sharp peak of 4,294 deaths in January 2022, marked by a blue diamond, then falls away and stays low through 2023. Months before August 2021 are shaded and drawn as a dotted line because the file undercounts them — March 2020 reads zero.

4,294 Pennsylvanians died of COVID-19 in January 2022 — the deadliest month of the pandemic in this state.

Not March 2020. Not the first wave.

What the timing means

The deadliest month arrived twenty-two months into the pandemic, after vaccines were widely available, during the Omicron wave. Cumulative deaths across these seven months crossed ten thousand during that same January.

The worst of an epidemic is not always its beginning, and a response calibrated to the first wave will misjudge the peak.

Challenge: the month it accelerated most

The deadliest month need not be the month deaths rose the most. Find the largest rise.

# `previous` must hold the *last* month's deaths when the loop reaches the
# current month. The first blank reads it; the second updates it, and it must
# happen after the comparison, at the end of every pass through the loop.
def biggest_jump(series): previous = None best_month = "" best_change = 0 for month, deaths in series.items(): if previous is not None: change = deaths - previous if change > best_change: best_change = change best_month = month previous = deaths return best_month, best_change month, change = biggest_jump(covid_deaths) print(f"Largest rise: {month}, up {change:,} deaths")
def biggest_jump(series):
    previous = None
    best_month = ""
    best_change = 0
    for month, deaths in series.items():
        if previous is not None:
            change = deaths - previous
            if change > best_change:
                best_change = change
                best_month = month
        previous = deaths
    return best_month, best_change
month, change = biggest_jump(covid_deaths)
print(f"Largest rise: {month}, up {change:,} deaths")

Your turn!

Why is if previous is not None needed? And why is the answer not January 2022?

Session outcomes

1. Storing labelled data in a dictionary, and retrieving it with [key] and .get()

2. Iterating with for, over a collection and over range()

3. Walking key–value pairs with .items()

4. Building results with the accumulator pattern, initialized outside the loop

5. Repeating with while, and exiting with while True: plus break

6. Defining functions with def, return, and a docstring

7. Returning several values, and unpacking tuples

8. Ordering with sorted(key=), and distinguishing counts from rates

9. Reading a small class: __init__, self, attributes, and methods

Preparation for the lab, Tuesday 10/20

The lab builds county-level dictionaries, writes functions returning several values, and ranks counties two ways.

Module 3 replaces these loops with arithmetic on whole arrays.

That’s all folks!

See you on Tuesday!