PUBHLT 0411 · Python for Public Health Data Analysis
Dept. of Biostatistics and Health Data Science, University of Pittsburgh
October 15, 2026
We will examine 49 months of COVID-19 death data across all 67 counties in Pennsylvania.
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(), +=.
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 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.
KeyErrorKeyError — 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 collectionIterating 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 integers1. 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).
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 loop retains a candidate and replaces it whenever a larger value appears. This is the same procedure a person performs by hand, stated precisely.
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 failsA 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 loopbreak 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.
The same calculation, written once and reused — instead of copied and edited every time a new county arrives.
def packages a computation under a name1. 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.
Returning several values produces a tuple, and assigning to several names unpacks it.
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 whatkey= receives a function returning the value to sort on. lambda writes that function inline.
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.
What the dot in .items(), .append(), and .strip() means.
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.
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.
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.
Putting the session together to answer the question it opened with - which month was the worst?
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")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.
The deadliest month need not be the month deaths rose the most. Find the largest rise.
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?
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.
See you on Tuesday!
soumik@pitt.edu · PUBHLT 0411 · Module 2: Collections, Loops, and Functions