PUBHLT 0411 · Python for Public Health Data Analysis
Dept. of Biostatistics and Health Data Science, University of Pittsburgh
November 10, 2026
Across all sixty-seven counties, adult asthma prevalence runs from 9.7% to 11.6%. The entire state fits inside two percentage points.
The question this session opens
A variable that hardly varies looks like a dead end. What else moves with it?
This session establishes the figure as an instrument: a chart is built to answer a question, read for what it shows, and checked for what it hides.
Required from Module 4: pd.read_csv(), boolean filtering, .groupby(), .merge(), and .isna().
1. The figure/axes model — the two objects every matplotlib figure is made of
2. Bar, histogram, scatter, and line, and the question each one answers
3. Labels, titles, ticks, legends, and saving a figure to a file
4. Multi-panel figures with plt.subplots()
5. The pandas .plot() shortcut, and seaborn for grouped figures
6. Accessibility as a requirement: alt text, contrast, and never colour alone
7. .describe(), grouped summaries, and .corr() as co-movement
8. Rates against counts — the error that survives every other check
9. EDA as a method: question → figure → better question
Descriptive statistics only
This course computes and interprets summaries, distributions, and correlations. It does not perform statistical inference — no p-values, no significance tests, no models.
1. The figure is the canvas: its size, its resolution, and the file it saves to
2. The axes is one set of coordinates drawn on that canvas: the data, the labels, the ticks
plt.subplots() returns both, and nearly every figure in practice begins with that call.
ax is not “axis”
An axes is an entire plotting area. An axis is one of its edges, reached as ax.xaxis or ax.yaxis. The naming is unfortunate and permanent.
ax.bar() compares a value across categoriesFigure 1. Bar chart of adult asthma prevalence for the five Pennsylvania counties with the highest rates: Philadelphia at 11.6 percent, then Fayette, Sullivan, Blair, and Clinton between 11.2 and 11.3 percent. The bars are nearly the same height.
Figure 2. Two bar charts of the same five county asthma rates side by side. The left panel has a y-axis starting at zero and the five bars look nearly identical. The right panel has a y-axis starting at 11 percent and the same five bars now look dramatically different in height.
Identical data. The right panel implies Philadelphia has roughly four times the asthma burden of Clinton County. The true difference is 0.4 percentage points.
The rule for bars
A bar encodes value by length, so its baseline must be zero. Truncating the axis breaks the correspondence between length and value.
ax.hist() shows the shape of one variableFigure 3. Histogram of adult asthma prevalence across all 67 Pennsylvania counties, with bars concentrated between 10 and 11.2 percent and a single vertical dashed line marking the state mean of 10.7 percent. The distribution is roughly symmetric and tightly clustered.
A bar chart compares named categories. A histogram bins a continuous variable and counts what falls in each bin.
ax.scatter() shows how two variables move togetherFigure 4. Scatter plot of adult asthma prevalence against adult smoking prevalence across 67 Pennsylvania counties. Points rise from left to right: counties near 11 percent smoking sit close to 9.8 percent asthma, and counties near 21 percent smoking sit near 11.3 percent asthma.
ax.plot() draws a value that moves through timeFigure 5. Line chart of monthly COVID-19 deaths across all Pennsylvania counties from March 2020 through March 2024. Two large peaks appear in winter 2020 and winter 2021, with the tallest at 4,294 deaths in January 2022, after which the line falls close to zero.
A line asserts that consecutive points are connected — that the value moved from one to the next. Categories are never connected by a line.
Figure 6. Two panels of the same scatter of asthma against smoking. The left panel has no axis labels or title and is uninterpretable. The right panel is fully labelled with axis names, units, a title, and a source note.
Both panels plot identical data. Only one of them can be read by someone who was not in the room when it was made.
The minimum for any figure that leaves a notebook
An x-label with units, a y-label with units, a title stating the claim, and a source.
savefig() writes the figure to a file1. dpi=200 sets the resolution; the default of 100 looks soft in a document
2. bbox_inches="tight" trims the surrounding whitespace and rescues cut-off labels
3. The extension chooses the format: .png for slides and documents, .pdf for print
savefig() before show()
plt.show() renders and then clears the figure. Calling savefig() afterwards writes a blank image.
plt.subplots() builds a grid of panelsFigure 7. Four-panel grid of Pennsylvania county health distributions: adult asthma prevalence, adult smoking prevalence, infant mortality per 1,000 births, and child poverty percent. Asthma is tightly clustered while the other three are visibly wider and more skewed.
plt.subplots(2, 2) returns a 2-D array of axes, indexed axes[row, col] — the same indexing NumPy introduced in Module 3.
A figure is invisible to a screen reader. Its alt text is the only description that reaches a reader who cannot see it.
Alt text states what the figure shows, not that it exists
✗ “Scatter plot of the data.” ✓ “Asthma prevalence rises with smoking prevalence across 67 counties, from 9.8% to 11.3%.”
Where the description goes depends on the tool
fig-alt works in a rendered Quarto document. It does not work in these slides — the figures here are drawn in the browser, and the attribute never reaches them. Every figure in this deck therefore carries its description as visible text underneath, which reaches a screen reader in either case.
University of Pittsburgh policy and the ADA Title II compliance date of April 2027 both require this. It applies to every figure submitted in this course, including the final project.
Figure 8. Two panels comparing metro and nonmetro counties on smoking and asthma. The left panel separates the groups by colour alone. The right panel uses colour, distinct marker shapes, and a legend, so the groups remain distinguishable without colour.
Text needs 4.5:1. A meaningful graphic — a bar, a point, a line that carries data — needs 3:1.
| Colour | Ratio on white | Verdict |
|---|---|---|
#003594 Pitt royal |
10.84 : 1 | Passes as text and as a mark |
#8a6400 dark gold |
5.38 : 1 | Passes as text and as a mark |
#6b7280 gray |
4.83 : 1 | Passes; reserved for annotation |
#B87333 Pitt bronze |
3.79 : 1 | Mark only — fails as text |
#FFB81C Pitt gold |
1.73 : 1 | Fails both. Never on white. |
Pitt gold is a brand colour, not a data colour
#FFB81C on white reaches 1.73:1. It is legible only as a fill behind dark text, or on the royal blue background used by the section dividers in this deck.
.plot() draws directly from a DataFrameFigure 9. Horizontal bar chart of mean adult asthma prevalence for six Pennsylvania regions, ranging from about 10.4 percent in the Southeast to 10.8 percent in North Central. The bars are close to equal in length.
.plot() on a Series or DataFrame calls matplotlib underneath and returns an axes, so every ax.set_*() method remains available.
seaborn is not bundled with this browser’s Python. micropip fetches it at runtime, and the download takes a few seconds on first use.
In JupyterLab, seaborn is already installed
import seaborn as sns is enough there — it ships with the course environment. The micropip step is specific to Python running inside a browser.
sns.boxplot() compares distributions across groupsFigure 10. Box plot of adult asthma prevalence for metro and nonmetro Pennsylvania counties. The two boxes overlap heavily, with nonmetro counties centred slightly higher at 10.7 percent against 10.5 percent for metro counties.
A box marks the median and the interquartile range; the whiskers reach the furthest points within 1.5 × IQR, and anything beyond is drawn individually.
histplot and scatterplot take the grouping as an argumentFigure 11. Two seaborn panels. The left is a histogram of adult smoking prevalence split by metro status, with metro counties concentrated at lower values. The right is a scatter of asthma against smoking, metro as circles and nonmetro as triangles, rising left to right.
style="metro" varies the marker shape alongside the colour — the second channel, supplied by one argument.
Every seaborn default palette contains colours below 3:1 against white:
| Palette | Lowest ratio | Failing colours |
|---|---|---|
deep (the default) |
2.80 : 1 | #DD8452, #55A868 |
colorblind |
2.61 : 1 | #DE8F05, #CA9161 |
muted |
2.01 : 1 | #EE854A, #6ACC64 |
tab10 |
2.53 : 1 | #FF7F0E |
A palette named colorblind is not an accessible palette
colorblind is designed so its hues stay distinguishable from one another under colour vision deficiency. That is a different property from contrast against the background, and it does not imply it.
This course passes an explicit palette= argument to every seaborn call.
sns.heatmap() shows a matrix of relationshipsFigure 12. Heat map of correlations among five maternal and infant health measures across 44 Pennsylvania counties, with numeric values printed in every cell. Child poverty and infant mortality correlate at 0.65, income and poverty at negative 0.81, and infant mortality with income at negative 0.60.
annot=True prints the value in every cell — the numbers are the second channel, so the figure survives being read without colour.
.describe() summarises every numeric column at onceEight numbers per column: the count, the mean, the standard deviation, the minimum, the three quartiles, and the maximum.
count is the first number to read
It reports non-missing values. A count below the number of rows means missing data is present in that column.
.groupby() and .agg() describe each groupThe comparison this table makes
Metro and nonmetro counties differ by 0.17 percentage points in asthma prevalence, and by 2.34 percentage points in smoking prevalence.
.corr() measures how two columns move togetherA correlation runs from −1 to +1. It reports the strength and direction of a straight-line relationship, and nothing else.
What a correlation cannot establish
It cannot establish cause. It cannot detect a curved relationship. It is distorted by outliers. And these are county averages, so it describes counties, not people.
Figure 13. Two scatter plots. The left panel shows adult smoking against asthma across 67 counties, an even upward cloud with correlation 0.659. The right panel shows the same data with one fabricated extreme point added at the top right, and the correlation rises to 0.827 on the strength of that single point.
Figure 14. Two horizontal bar charts of Pennsylvania COVID-19 mortality. The left ranks counties by total deaths and lists Philadelphia, Allegheny, Montgomery, Lancaster, and Bucks — the five largest counties. The right ranks by deaths per 100,000 and lists Potter, McKean, Juniata, Jefferson, and Armstrong — five small rural counties. The two lists share no counties.
The two lists share no counties.
import pandas as pd
covid = pd.read_csv("data/pa_covid_monthly.csv")
counties = pd.read_csv("data/pa_counties.csv")
totals = (covid.groupby("county", as_index=False)["covid_deaths"].sum()
.merge(counties[["county", "population_2023"]], on="county"))
totals["rate_per_100k"] = totals["covid_deaths"] / totals["population_2023"] * 100000
print("deaths vs population:", round(totals["covid_deaths"].corr(totals["population_2023"]), 3))
print("rate vs population:", round(totals["rate_per_100k"].corr(totals["population_2023"]), 3))
import pandas as pd
covid = pd.read_csv("data/pa_covid_monthly.csv")
counties = pd.read_csv("data/pa_counties.csv")
totals = (covid.groupby("county", as_index=False)["covid_deaths"].sum()
.merge(counties[["county", "population_2023"]], on="county"))
totals["rate_per_100k"] = totals["covid_deaths"] / totals["population_2023"] * 100000
print("deaths vs population:", round(totals["covid_deaths"].corr(totals["population_2023"]), 3))
print("rate vs population:", round(totals["rate_per_100k"].corr(totals["population_2023"]), 3))1. Ask — one question the data could answer
2. Draw — the simplest figure that addresses it
3. Read — what the figure shows, and what it excludes
4. Ask again — the sharper question the figure exposed
This session has run that loop three times.
| Question | Figure | What it produced |
|---|---|---|
| How much does asthma vary? | Histogram | Very little — 9.7% to 11.6% |
| Does region explain it? | Bar chart by region | No — 0.4 points across six regions |
| Does metro status explain it? | Box plot | No — boxes overlap almost entirely |
Three negative answers narrowed the question to the one the data can answer.
Figure 15. Scatter plot of adult asthma prevalence against adult smoking prevalence across all 67 Pennsylvania counties, with metro counties as circles and nonmetro counties as triangles. Points rise clearly from left to right, correlation 0.659. Chester County sits at the low left with 10.6 percent smoking and 9.7 percent asthma; Sullivan County sits at the upper right with 21.1 percent smoking and 11.3 percent asthma.
1. The figure/axes model, and plt.subplots() as the entry point to every figure
2. ax.bar(), ax.hist(), ax.scatter(), ax.plot() — and the question each answers
3. Zero baselines for bars; axis truncation as a distortion
4. Labels, units, titles, sources, legends, and savefig() with bbox_inches="tight"
5. Multi-panel figures indexed axes[row, col]
6. pandas .plot(); seaborn boxplot and heatmap via micropip
7. Accessibility: alt text on every figure, marks at 3:1, and colour never alone
8. .describe(), grouped .agg(), and .corr() as co-movement, never cause
9. Rates against counts — the denominator question
10. EDA: question → figure → better question
Preparation for the lab, Thursday 11/12
The lab builds twelve figures from the five course datasets, including a full multi-panel figure with alt text, and repairs three figures that are misleading rather than broken.
Module 6 assembles the entire pipeline — import, clean, join, group, summarise, visualise, report — into one reproducible document.
See you on Thursday!
soumik@pitt.edu · PUBHLT 0411 · Module 5: Visualisation and description