Rain falls, the river answers days later. This week the code can finally see the lag.
A big storm hits Iowa City
Two records, one table
| File | Source | Coverage | Used for |
|---|---|---|---|
| iowa_streamflow_daily.csv | USGS NWIS, site 05454500, Iowa River at Iowa City | Daily, 2015–2025, 4,018 rows | Groupby, resample, rolling, the anomaly |
| iowa_city_weather_daily.csv | Iowa Environmental Mesonet, station IOW | Daily, 2019–2025, 2,549 rows | Precipitation, and the merge |
A table where every column has a name and every row has a label.
ndarray
DataFrame
Python
import pandas as pd
flow = pd.Series(
[10600.0, 10700.0, 11600.0, 12600.0],
index=["May 26", "May 27", "May 28", "May 29"],
name="discharge_cfs")
flow
print(flow["May 29"]) # by label
print(flow.max(), flow.idxmax()) # value, labelOutput
May 26 10600.0 May 27 10700.0 May 28 11600.0 May 29 12600.0 Name: discharge_cfs, dtype: float64 12600.0 12600.0 May 29
Python
may = pd.DataFrame({
"date": ["2019-05-27", "2019-05-28", "2019-05-29"],
"temp_mean_c": [21.0, 16.4, 18.3],
"precipitation_mm": [8.9, 42.7, 20.8],
"discharge_cfs": [10700.0, 11600.0, 12600.0],
})
may
may.shape # (rows, columns)Output
date temp_mean_c precipitation_mm discharge_cfs 0 2019-05-27 21.0 8.9 10700.0 1 2019-05-28 16.4 42.7 11600.0 2 2019-05-29 18.3 20.8 12600.0 (3, 4)
Python
from earthcourse.io import read_processed_csv # wraps pd.read_csv
weather = read_processed_csv("iowa_city_weather_daily.csv")
weather.shape
weather[["date", "temp_mean_c", "precipitation_mm"]].head(3)Output
(2549, 13)
date temp_mean_c precipitation_mm
0 2019-01-01 -2.099151 0.0
1 2019-01-02 -4.923418 0.0
2 2019-01-03 -1.703704 0.0Python
weather.dtypes # object = text (Python strings) # float64 = numbers you can compute on # date is text for now. # Part 2 fixes that.
Output
date object station object temp_mean_c float64 temp_min_c float64 temp_max_c float64 dewpoint_mean_c float64 relative_humidity_mean float64 wind_speed_mean_mps float64 precipitation_mm float64 latitude float64 longitude float64 elevation_m float64 source_type object dtype: object
Python
temp = weather["temp_mean_c"] # a name type(temp) pair = weather[["date", "precipitation_mm"]] # a list type(pair), pair.shape print(round(weather["temp_mean_c"].mean(), 2))
Output
<class 'pandas.core.series.Series'> (<class 'pandas.core.frame.DataFrame'>, (2549, 2)) 11.08
Python
weather.iloc[:2, :3] # rows 0-1, columns 0-2 wet = weather["precipitation_mm"] > 50 print(wet.sum()) # True counts as 1 weather.loc[wet, ["date", "precipitation_mm"]]
Output
date station temp_mean_c
0 2019-01-01 IOW -2.099151
1 2019-01-02 IOW -4.923418
6
date precipitation_mm
525 2020-06-09 88.392
1027 2021-10-24 56.642
1271 2022-06-25 54.356
1918 2024-04-02 59.436
1997 2024-06-20 67.310
2053 2024-08-15 64.262Python
weather["temp_range_c"] = weather["temp_max_c"] - weather["temp_min_c"] weather["wet_day"] = weather["precipitation_mm"] > 0 weather[["temp_max_c", "temp_min_c", "temp_range_c", "wet_day"]].head(3) print(weather["wet_day"].sum(), "wet days of", len(weather))
Output
temp_max_c temp_min_c temp_range_c wet_day 0 1.111111 -5.611111 6.722222 False 1 -1.569444 -6.722222 5.152778 False 2 7.222222 -6.722222 13.944444 False 718 wet days of 2549
Python
cols = ["temp_mean_c", "wind_speed_mean_mps",
"precipitation_mm"]
weather[cols].isna().sum()
gaps = weather["temp_mean_c"].isna()
weather.loc[gaps, ["date", "temp_mean_c"]]
len(weather.dropna(subset=["temp_mean_c"]))Output
temp_mean_c 3
wind_speed_mean_mps 2
precipitation_mm 0
dtype: int64
date temp_mean_c
726 2020-12-27 NaN
2130 2024-10-31 NaN
2141 2024-11-18 NaN
2546Python
stream = read_processed_csv("iowa_streamflow_daily.csv")
stream["discharge_cfs"].describe().round(1)
peak = stream["discharge_cfs"].idxmax()
stream.loc[peak, ["date", "discharge_cfs"]]Output
count 4018.0 mean 2712.5 std 2682.5 min 89.3 25% 700.0 50% 1850.0 75% 3837.5 max 14800.0 Name: discharge_cfs, dtype: float64 date 2018-10-06 discharge_cfs 14800.0 Name: 1374, dtype: object
?
What does weather["precipitation_mm"] > 50 return: a number, a DataFrame, or something else? And what does weather[ that ] return?
Answer
The index stops being a row number and becomes a date.
Python
weather["date"].dtype # the file we loaded in Part 1
len(weather[weather["date"] >= "2019-6-1"]) # text compared to text
parsed = read_processed_csv("iowa_city_weather_daily.csv", parse_dates=["date"])
len(parsed[parsed["date"] >= "2019-6-1"]) # dates compared to a dateOutput
dtype('O')
2184
2398Python
weather = read_processed_csv("iowa_city_weather_daily.csv",
parse_dates=["date"])
stream = read_processed_csv("iowa_streamflow_daily.csv",
parse_dates=["date"])
weather["date"].dtype
weather["date"].dt.month # 1 ... 12
weather["date"].dt.dayofyear # 1 ... 366
weather["date"].dt.year.value_counts().sort_index()Output
dtype('<M8[ns]')
date
2019 365
2020 366
2021 365
2022 365
2023 365
2024 359
2025 364
Name: count, dtype: int64Python
w = weather.set_index("date")
print(w.loc["2019-05", "precipitation_mm"].sum().round(1))
cols = ["temp_mean_c", "precipitation_mm"]
w.loc["2019-05-26":"2019-05-29", cols].round(1)
flow = stream.set_index("date")["discharge_cfs"]
print(flow.loc["2019-05-29"])Output
255.3
temp_mean_c precipitation_mm
date
2019-05-26 21.7 6.1
2019-05-27 21.0 8.9
2019-05-28 16.4 42.7
2019-05-29 18.3 20.8
12600.0Typical versus actual: groupby, resample, rolling, and the anomaly between them.
Python
clim = (stream
.groupby(stream["date"].dt.month)
["discharge_cfs"]
.mean()
.round(1))
clim
# split = rows into 12 month-groups
# apply = mean within each group
# combine = a 12-row tableOutput
date 1 1817.0 2 1756.5 3 3365.7 4 3415.6 5 3896.4 6 4412.1 7 3649.0 8 1922.1 9 1711.7 10 2538.2 11 1969.9 12 2032.3 Name: discharge_cfs, dtype: float64
Python
monthly = (weather.set_index("date")
.resample("MS")
.agg({"temp_mean_c": "mean",
"precipitation_mm": "sum"})
.round(1))
monthly.loc["2019-01":"2019-08"]Output
temp_mean_c precipitation_mm date 2019-01-01 -6.7 28.4 2019-02-01 -6.1 67.6 2019-03-01 1.4 51.1 2019-04-01 10.6 112.5 2019-05-01 15.2 255.3 2019-06-01 21.9 81.8 2019-07-01 26.5 34.0 2019-08-01 22.9 122.2
12 rows, the climatology
One row per actual month
Python
flow = stream.set_index("date")["discharge_cfs"]
flow = flow.sort_index()
week = flow.rolling(7, min_periods=7).mean()
tbl = pd.DataFrame({"daily": flow,
"rolling_7d": week})
tbl.loc["2019-05-26":"2019-06-01"].round(1)Output
daily rolling_7d date 2019-05-26 10600.0 6627.1 2019-05-27 10700.0 7827.1 2019-05-28 11600.0 9204.3 2019-05-29 12600.0 10425.7 2019-05-30 12300.0 11125.7 2019-05-31 11100.0 11428.6 2019-06-01 10700.0 11371.4
Python
from earthcourse.hydro import annual_peak_flow, seven_day_low_flow peaks = annual_peak_flow(stream) # groupby year, then idxmax low7 = seven_day_low_flow(stream) # reindex every day, rolling(7, min_periods=7) peaks.merge(low7, on="year").round(1).tail(4)
Output
year peak_date peak_discharge_cfs low7_discharge_cfs 7 2022 2022-05-06 6770.0 172.1 8 2023 2023-03-04 6480.0 89.3 9 2024 2024-05-28 10600.0 140.4 10 2025 2025-08-10 10700.0 293.3
Python
s = stream.set_index("date")["discharge_cfs"]
m = s.resample("MS").mean() # actual
clim = m.groupby(m.index.month).mean() # typical
anom = m - m.index.month.map(clim)
anom.loc["2024"].round(1)Output
date 2024-01-01 -1645.4 2024-02-01 -1262.7 2024-03-01 -3030.9 2024-04-01 -2228.1 2024-05-01 1980.7 2024-06-01 1295.2 2024-07-01 1105.8 2024-08-01 -126.1 2024-09-01 -1348.7 2024-10-01 -2361.9 2024-11-01 -1097.5 2024-12-01 -1070.8 Freq: MS, Name: discharge_cfs, dtype: float64
?
Which tool answers: was May 2019 an unusual month for the Iowa River?
Answer
Two files, two date ranges, one table. Count rows every time.
Python
merged = weather.merge(stream[["date", "discharge_cfs"]], on="date", how="inner") print(len(weather), len(stream), len(merged)) merged["date"].min(), merged["date"].max()
Output
2549 4018 2549
(Timestamp('2019-01-01 00:00:00'), Timestamp('2025-12-30 00:00:00'))?
Weather starts in 2019, streamflow in 2015. After an inner merge on date, what date range survives, and where did 2015-2018 go?
Answer
Python
flow = stream.set_index("date")["discharge_cfs"]
event = pd.Timestamp("2024-06-20")
window = flow.loc[event : event + pd.Timedelta(days=5)]
print(window.iloc[0]) # flow on the rain day
print(window.max()) # peak within five days
print(window.to_numpy().argmax()) # days until that peakOutput
2660.0 3600.0 4
Python
events = merged.nlargest(5, "precipitation_mm")
rows = []
for day, rain in zip(events["date"], events["precipitation_mm"]):
w = flow.loc[day : day + pd.Timedelta(days=5)]
rows.append({"date": day.date(), "rain_mm": round(rain, 1), "flow_day0": w.iloc[0],
"peak_cfs": w.max(), "days_to_peak": (w.idxmax() - day).days})
pd.DataFrame(rows)Output
date rain_mm flow_day0 peak_cfs days_to_peak 0 2020-06-09 88.4 7700.0 7700.0 0 1 2024-06-20 67.3 2660.0 3600.0 4 2 2024-08-15 64.3 3070.0 3070.0 0 3 2024-04-02 59.4 1820.0 2210.0 1 4 2021-10-24 56.6 537.0 1240.0 5
Build the summaries
Ask the questions
Python
daily_r = merged["precipitation_mm"].corr(merged["discharge_cfs"])
monthly = (merged.set_index("date").resample("MS")
.agg({"precipitation_mm": "sum", "discharge_cfs": "mean"}))
monthly_r = monthly["precipitation_mm"].corr(monthly["discharge_cfs"])
print(f"daily r = {daily_r:.3f} monthly r = {monthly_r:.3f}")Output
daily r = 0.107 monthly r = 0.428
?
Estimate before you compute: how many days should the Iowa River take to peak after a big rain, and what controls that number?
For Wednesday