PUBHLT 0411 · Python for Public Health Data Analysis
Dept. of Biostatistics and Health Data Science, University of Pittsburgh
October 8, 2026
Two Pennsylvania counties. Populations within 10% of one another. One buries nine times as many residents to overdose as the other.
Figure 1. Centre County, a gold square at 4.4 deaths per 100,000, sits near the low end of the distribution; Schuylkill County, a blue diamond at 41.7, sits in the upper third. An arrow spans the two, labelled 9.4x. The state rate of 36.7 is marked by a dashed line.
The question this session opens
Module 0 established the two rates. This session takes that rate and implements a classification: given a county’s deaths and population, the code decides whether its burden is low, moderate, or high — and states the evidence.
Required from Module 0: assignment, int and float, arithmetic, f-strings.
A comparison is an expression. It evaluates to True or False.
| Operator | Meaning |
|---|---|
== |
equal to |
!= |
not equal to |
< > |
less than, greater than |
<= >= |
less than or equal, greater than or equal |
= assigns; == compares
rate = 41.7 binds a name. rate == 41.7 asks a question. Substituting one for the other is among the most frequent errors in introductory Python.
1. and — True only when both sides are True
2. or — True when either side is True
3. not — inverts the value
R analogue
R’s && and || correspond to and and or. Python spells them as words, and provides no && operator for this purpose.
if executes a block when a condition holdsTwo pieces of syntax are mandatory:
1. The colon ends the if line.
2. The indentation — four spaces — marks the conditional block.
Figure 2. Each point is a county, with population on a log scale across and overdose rate up. A dashed line at the state rate of 36.7 divides them: 16 counties lie above it, including Schuylkill; 44 lie at or below it, including Centre.
elif and else handle the remaining casesEvaluation stops at the first True branch. Order therefore determines behavior.
The two thresholds are real:
1. 36.7 per 100,000: Pennsylvania’s overdose mortality rate, 2023
2. 13.1 per 100,000: the Healthy People 2030 target
Only one branch executes
Once a condition matches, the remaining branches are not evaluated. A chain ordered from lowest threshold upward reports the wrong category for every value.
Figure 3. The same counties, now in three bands. Above the state rate of 36.7: High, 16 counties, including Schuylkill. Between the Healthy People 2030 target of 13.1 and the state rate: Moderate, 39 counties. Below 13.1: Low, 5 counties, including Centre.
Objective: report whether Schuylkill County exceeds the state rate of 36.7 per 100,000.
A silent error: the comparison is valid but meaningless
The code runs and prints an answer. It compares a count (60 deaths) against a rate (36.7 per 100,000) — quantities in different units. The comparison is arithmetically legal, so no exception is raised.
Resolution: compare rate_per_100k > 36.7. The correct answer is the same here, which is what makes the error survive review.
total_deaths += 60 is equivalent to total_deaths = total_deaths + 60.
The same form exists for the other operators: -=, *=, /=.
Read the statement as an instruction, not an equation
total_deaths = total_deaths + 60 is false as algebra and correct as a sequence of steps: evaluate the right side, then rebind the name.
A sequence is a generic term for an ordered collection of items where each element can be efficiently accessed using an integer index
Each character occupies a position, and positions are numbered from zero.
The first position is 0, and the last is len() - 1
county[10] raises IndexError: string index out of range, because the valid positions of a ten-character string are 0 through 9. Negative indices count backward from the end.
sequence[start:stop] includes start and excludes stop.
1. Omitting start begins at the beginning
2. Omitting stop continues to the end
3. A third value is the step: [::2] takes every second character
The stop position is excluded
county[0:4] yields Schu — four characters, positions 0 through 3. The length of a slice is stop - start, which is the reason the convention is convenient.
A method is called on the value itself, using a dot.
| Method | Effect |
|---|---|
.strip() |
removes leading and trailing whitespace |
.upper() |
converts to uppercase |
.replace(a, b) |
substitutes every occurrence of a with b |
.split(sep) |
divides into a list at each separator |
Methods return new values
county.upper() produces a new string. The original binding is unchanged unless it is reassigned.
.split() divides a line into fieldsThe result is a list — an ordered sequence of values, indexed from zero exactly as a string is.
Fields extracted from text are text
fields[2] is the string "60", not the number 60. Arithmetic requires explicit conversion with int() or float().
Lists are indexed and sliced with the same notation as strings. Position i in counties corresponds to position i in rates — the two lists are parallel.
R analogue
An R vector, with one difference: a Python list may hold values of mixed type, and it does not recycle.
1. .append(value) adds one value to the end
2. value in sequence tests membership and returns a boolean
.append() returns nothing
The list is modified where it stands. Writing counties = counties.append("Franklin") rebinds the name to None and discards the list.
Objective: print a warning for a county above the state rate.
IndentationError — the block is missing
The reported message is expected an indented block after 'if' statement on line 3.
A colon at the end of a line commits the following line to be indented. Python defines the conditional block by that indentation, so an unindented line leaves the if with no body.
Resolution: indent the print() by four spaces.
Population size and overdose rate together determine how resources are allocated — and a county’s classification is the first step in that decision.
Franklin County, 2023: 30 deaths, population 157,854.
county = "Franklin"
overdose_deaths = 30
population = 157854
rate_per_100k = overdose_deaths / population * 100000
if rate_per_100k >= 36.7:
classification = "High"
elif rate_per_100k >= 13.1:
classification = "Moderate"
else:
classification = "Low"
print(f"{county}: {rate_per_100k:.1f} per 100,000 — {classification}")
county = "Franklin"
overdose_deaths = 30
population = 157854
rate_per_100k = overdose_deaths / population * 100000
if rate_per_100k >= 36.7:
classification = "High"
elif rate_per_100k >= 13.1:
classification = "Moderate"
else:
classification = "Low"
print(f"{county}: {rate_per_100k:.1f} per 100,000 — {classification}")The second variable is the count behind the rate. Elk’s rate is high, but it rests on 14 deaths: one more or one fewer moves it by more than 3 per 100,000.
This threshold is real too
The National Center for Health Statistics marks a death rate as unreliable when it is based on fewer than 20 deaths.
Figure 4. The same counties, classified on rate and count together. A dotted curve marks where a county has exactly 20 deaths; counties to its left have fewer. Above the state rate and right of the curve: Priority, 14 counties, including Schuylkill. Above the state rate but left of the curve: Watch, 2 counties, Elk and Susquehanna. Below the state rate: 44 counties, including Centre, whose rate rests on 7 deaths.
1. Evaluating comparisons and combining them with and, or, not
2. Branching with if / elif / else, and the colon and four-space indent
3. Accumulating with +=
4. Indexing and slicing strings and lists from position zero
5. Applying .strip(), .upper(), .replace(), and .split()
6. Building lists, extending them with .append(), and testing membership with in
Preparation for the lab, Tuesday 10/13
The lab classifies additional counties, parses real record lines, and builds parallel lists. No new syntax is required beyond this session.
Module 2 addresses scale: sixty-seven counties and forty-nine months, without repetition.
One line of the 2023 file, as text. Split it, convert the fields, and classify the county.
line = "Montour,2023,20,17860"
fields = line.split(",")
county = fields[0]
overdose_deaths = int(fields[2])
population = int(fields[3])
rate_per_100k = overdose_deaths / population * 100000
if rate_per_100k >= 36.7 and overdose_deaths >= 20:
classification = "Priority"
elif rate_per_100k >= 36.7:
classification = "Watch"
else:
classification = "Below state"
print(f"{county}: {rate_per_100k:.1f} on {overdose_deaths} deaths — {classification}")
line = "Montour,2023,20,17860"
fields = line.split(",")
county = fields[0]
overdose_deaths = int(fields[2])
population = int(fields[3])
rate_per_100k = overdose_deaths / population * 100000
if rate_per_100k >= 36.7 and overdose_deaths >= 20:
classification = "Priority"
elif rate_per_100k >= 36.7:
classification = "Watch"
else:
classification = "Below state"
print(f"{county}: {rate_per_100k:.1f} on {overdose_deaths} deaths — {classification}")Your turn!
Centre’s rate of 4.4 rests on 7 deaths. Does “ninefold” still stand?
Your turn!
Montour has exactly 20 deaths. Should >= 20 or > 20 decide its class?
See you on Tuesday!
soumik@pitt.edu · PUBHLT 0411 · Module 1: Conditions and Sequences