Downloading, summarizing, and modeling Duluth weather station data
2026-07-05
lm(area_cm2 ~ mass_g, data = paper_df)summary() — slope, intercept, R², p-valuepredict() with confidence and prediction intervalsGSODRgroup_by() + summarize()lm()) to estimate the rate of temperature change over timecase_when() to build a summer vs. winter season variableHow to use this worksheet
- Work through each part in order. Type the code into a new R script in Positron and run it line by line.
- Code blocks marked ▶ Run this should be executed as written.
- Blocks marked ✏️ Your turn ask you to write, modify, or interpret.
- The Going further section is optional.
🔮 Predict before you run — and type, don’t paste
Before each ▶ Run this block, cover the output and predict what R will print. Then type the code yourself. Two reasons:
temp, == instead of %in%) that you would otherwise spend real time hunting.🧩 Chunk 1 — Download & explore raw data (after lecture Chunk 1)
Parts 1–3: download the Duluth record, trim the columns, and plot the raw daily data.
▶ Run this at the top of your script:
We will use the Duluth International Airport station (727450-14913), which has daily records back to 1948.
▶ Run this (this may take a minute — it’s downloading real data):
✏️ Your turn: Run dim(duluth_df) and glimpse(duluth_df). How many rows and columns does the raw download have?
Rows:
Columns:
What does one row represent (a year? a month? a day?):
▶ Run this:
✏️ Your turn: Why do we use select() here instead of just working with the full duluth_df? (Hint: how many columns did get_GSOD() actually return?)
Your answer:
🔮 Predict first: this plots ~28,000 daily points. Before you run it — will a warming trend be visible, or will something else dominate? Write your guess.
▶ Run this:
# Plot every single daily temperature -----------------------
raw_temp_plot <- dlh_temp_df %>%
ggplot(aes(x = YEARMODA, y = TEMP)) +
geom_point(alpha = 0.2, size = 0.5) +
geom_line(alpha = 0.3) +
labs(
title = "Duluth Daily Mean Temperature, 1948–2025",
x = "Date",
y = "Temperature (°C)"
) +
theme_minimal()
raw_temp_plot✏️ Your turn: Describe what you see. Can you tell whether Duluth is warming over time just by looking at this plot? Why or why not?
What dominates the plot (seasonal cycle / long-term trend / both)?
Can you see a clear warming trend? Y / N
Why or why not:
💡 Key idea: Raw data shows you everything that happened — which can hide the one pattern you’re looking for. The seasonal cycle (summer ↔︎ winter) is much bigger than any year-to-year warming trend, so it drowns the trend out visually.
🧩 Chunk 2 — Summarize to reveal the trend (after lecture Chunk 2)
Parts 4–5: collapse the daily data to monthly and yearly means.
▶ Run this:
# Plot the monthly means --------------------------------------
month_temp_plot <- dlh_month_df %>%
ggplot(aes(x = YEARMODA, y = TEMP)) +
geom_point(alpha = 0.4, size = 0.8) +
geom_line(alpha = 0.4) +
geom_smooth(method = "lm", color = "steelblue") +
labs(
title = "Duluth Monthly Mean Temperature",
x = "Date", y = "Temperature (°C)"
) +
theme_minimal()
month_temp_plot✏️ Your turn: How many rows does dlh_month_df have compared to dlh_temp_df? What pattern is now visible that wasn’t visible in the raw daily plot?
Rows in dlh_temp_df (daily):
Rows in dlh_month_df (monthly):
New pattern now visible:
▶ Run this:
✏️ Your turn: Fill in the table comparing all three levels of summary:
Level Rows What it shows clearly What it hides
-------- ------ ------------------------- -------------------------
Daily
Monthly
Yearly
✏️ Your turn: Which summary level would you use if you wanted to know about a single extreme heat wave? Which would you use to study climate change? Explain why they’re different.
Best level for a single heat wave:
Best level for climate change:
Why they're different:
🧩 Chunk 3 — Model the rate of change (after lecture Chunk 3)
Part 6: fit
lm(TEMP ~ YEAR)and read the warming slope.
lm()🔮 Predict first: will the slope of
TEMP ~ YEARbe positive or negative? Roughly how many °C per decade? Guess before you runsummary().
▶ Run this:
✏️ Your turn: This is the exact same lm() syntax from Worksheet 05. Fill in what X and Y are in this new context:
Worksheet 05: X = mass_g Y = area_cm2
Worksheet 06: X = _____ Y = _____
▶ Run this:
✏️ Your turn: Report the results from your summary() output:
Slope (b) =
p-value for the slope =
R-squared =
Warming rate in °C per decade =
Is the warming trend statistically significant at α = 0.05? Y / N
📖 Whitlock & Schluter §17.3: the slope’s p-value tests H₀: β = 0. A significant slope means temperature is changing systematically with year — not just by chance.
🧩 Chunk 4 — Seasons: is winter warming faster? (after lecture Chunk 4)
Parts 7–11: build the season variable, model each season, and write the results paragraph.
case_when()🔮 Predict first: which season category (summer / winter / shoulder) will have the most rows in the
count()? Predict before running.
We now ask a sharper question: is winter warming at the same rate as summer?
▶ Run this:
# Label each day as summer, winter, or shoulder season -------------
dlh_season_df <- dlh_temp_df %>%
mutate(
season = case_when(
MONTH %in% c(6, 7, 8) ~ "summer",
MONTH %in% c(12, 1, 2) ~ "winter",
TRUE ~ "shoulder"
)
)
# Quick check - did it work? -----------------------------------------
dlh_season_df %>%
count(season)✏️ Your turn: How many rows fall into each season category? Does the count roughly make sense given there are 12 months and 3 are assigned to summer, 3 to winter?
n(summer):
n(winter):
n(shoulder):
Does this make sense? Y / N
✏️ Your turn: In your own words, explain what each line of the case_when() is doing. What does TRUE ~ "shoulder" mean?
What MONTH %in% c(6, 7, 8) ~ "summer" does:
What TRUE ~ "shoulder" does:
▶ Run this:
✏️ Your turn: What does group_by(YEAR, season) do differently from group_by(YEAR) alone? Why do we need both?
Your answer:
▶ Run this:
# Plot both seasons with separate trend lines --------------------------
season_temp_plot <- season_year_df %>%
ggplot(aes(x = YEAR, y = TEMP, color = season)) +
geom_point(size = 1.8, alpha = 0.7) +
geom_smooth(method = "lm", se = TRUE) +
scale_color_manual(values = c(
"summer" = "firebrick",
"winter" = "steelblue"
)) +
labs(
title = "Duluth Summer vs. Winter Temperature Trends",
x = "Year", y = "Mean Temp (°C)",
color = "Season"
) +
theme_minimal()
season_temp_plot✏️ Your turn: Just from looking at the plot, which trend line looks steeper — summer or winter? Make a prediction before you run the models in the next part.
Visual prediction — steeper line:
🔮 Predict first: commit now — which season warms faster, summer or winter? Write your guess before the two slopes print.
▶ Run this:
✏️ Your turn: Fill in the comparison table:
Season Slope (°C/year) Rate (°C/decade) p-value Significant? (Y/N)
------- ---------------- ------------------ -------- -------------------
Summer
Winter
✏️ Your turn: Was your visual prediction from Part 9 correct? Which season is warming faster in Duluth’s data? Does this match what you might expect from the news or your own experience with winters here?
Faster-warming season:
Did this match your prediction? Y / N
Does this match your real-world experience? Y / N
Why might winter and summer warm at different rates? (Hint: think about ice cover, snow reflectivity, cloud cover)
✏️ Your turn: Write a short paragraph (4–6 sentences) summarizing what you found. Include:
Write your results paragraph here:
At this point you should be able to:
✏️ Your turn — before you move on: Run your entire script with Ctrl/Cmd + Shift + Enter. Does it run from top to bottom without errors?
Ran cleanly? Y / N
If not, what error appeared:
This section is optional — work through it if you finish early or want to push deeper.
▶ Try this: Instead of strict 3-month summer/winter, try defining seasons using meteorological vs. astronomical boundaries, or add a “spring” and “fall” category to the case_when().
# Add all four seasons instead of just summer/winter -----------------
dlh_four_season_df <- dlh_temp_df %>%
mutate(
season = case_when(
MONTH %in% c(3, 4, 5) ~ "spring",
MONTH %in% c(6, 7, 8) ~ "summer",
MONTH %in% c(9, 10, 11) ~ "fall",
MONTH %in% c(12, 1, 2) ~ "winter"
)
)
dlh_four_season_df %>% count(season)✏️ Your turn: Does adding spring and fall change your interpretation at all?
Your answer:
▶ Try this:
# Explore snow/ice flag data -------------------------------------------
dlh_snow_df <- dlh_temp_df %>%
mutate(
snow_mm = case_when(
I_SNOW_ICE == 1 ~ PRCP,
TRUE ~ NA
)
) %>%
mutate(snow_mm = ifelse(snow_mm == 0, NA, snow_mm))
yearly_snow_df <- dlh_snow_df %>%
group_by(YEAR) %>%
summarize(sum_snow = sum(snow_mm, na.rm = TRUE))
yearly_snow_df %>%
ggplot(aes(x = YEAR, y = sum_snow)) +
geom_point() +
geom_line() +
geom_smooth(method = "lm") +
labs(x = "Year", y = "Total Yearly Snow (mm)") +
theme_minimal()✏️ Your turn: Has total yearly snowfall changed over time the same way temperature has? Why might these two trends differ?
Your answer:
figures/ folder should contain after this worksheetfigures/
├── raw_temp_plot.png ← from Part 3
├── month_temp_plot.png ← from Part 4
├── yearly_temp_plot.png ← from Part 5
├── season_temp_plot.png ← from Part 9
get_GSOD() taking forever / failing: check your internet connection — this function reaches out to NOAA’s servers. If it keeps failing, ask for the saved .csv backup.select() error — object not found: run names(duluth_df) first to confirm exact column names; capitalization matters (TEMP, not temp).case_when() returns all NA: check that your conditions use %in% for multiple values (MONTH %in% c(6,7,8)), not == with a vector.group_by() summary looks wrong: always check with head() immediately after summarizing — did you group by the right combination of variables?color = season is inside aes(), and that scale_color_manual() values exactly match your case_when() labels ("summer", "winter").💡 Key idea: Every dataset you’ll ever download from NOAA, eBird, or any other public source will need this same workflow — download → trim → explore raw → summarize → model. You now have it.
End of Worksheet 06. Next: Homework — repeat this entire workflow for a city of your choice and compare your city’s warming rate to Duluth’s.