Earth & Environmental Computing · Week 08 · Course session · 45 slides
Download HTML
Earth & Environmental Computing · Computation in the Earth and Environment · Fall 2026

Statistics, Regression, and Uncertainty

The fit takes one line of code. Knowing what it is worth takes the rest of the lecture.

Week 08·Course session·Open Course Materials | Adapt for your classroom
Learning objectives

By the end of this lecture, you will be able to

  • Summarize a variable's shape before modeling it, and choose its units
  • Read a correlation, and name a third variable that could produce it
  • Fit a least-squares line and translate its slope into flow units
  • Diagnose a fit from the pattern of its residuals
  • Score a model on years it never saw, against a simple baseline
  • State a result with the verb its evidence supports
Contents

Today's Lecture

Part 1 · Concepts

1Mean and median
2Skew and the log transform
3Correlation, r
4Least squares
5Slope and intercept
6r²
7RMSE

Part 2 · The Iowa River

01Look at it first
02Correlation and its traps
03Least squares on our data
04Critique and rebuild
05Residuals
06Honest evaluation

Part 3 · Group Lab 2

07How the lab runs
08Pitfalls
09Reference and takeaways
Recap

Last Week in Sixty Seconds

  • One figure, one claim, said in one sentence
  • Work through fig, ax; every element has a method
  • Log axes when data span orders of magnitude
  • Today: put a number on the claim, and say how sure it is
1

Concepts

Seven measures, each worked by hand on five numbers.

Part 1 · Concept 1 of 7

Mean and Median

Definition

The mean is the sum divided by the count. The median is the middle value once sorted.

Equation

x̄ = (1/n) sum of xi
median = the middle value, once sorted

Five days of flow, hundreds of cfs

Sorted:  2, 3, 3, 4, 18

mean   = (2+3+3+4+18) / 5
       = 30 / 5 = 6

median = 3rd of 5 sorted = 3
One flood day pulls the mean up; the median stays at 3. Without it, both are 3.
Part 1 · Concept 2 of 7

Skew and the Log Transform

Definition

Skew measures how lopsided values are: 0 is symmetric, positive means a long right tail. log10 turns equal ratios into equal steps.

Equation

g = (1/n) sum of (xi − x̄s)3
z = log10 x,   so   x = 10z

Flow that doubles each step

Raw:    100, 200, 400, 800, 1600 cfs
        g = 0.89

log10:  2.0, 2.3, 2.6, 2.9, 3.2
        g = 0

+0.30 in log10 = ×2 in cfs;  +1 = ×10
Same five values. On the raw axis the gaps grow; in log10 they are equal.
Part 1 · Concept 3 of 7

Correlation, r

Definition

r measures how closely points fall along one straight line, from −1 to +1. It says nothing about why.

Equation

r = sum of (xi − x̄)(yi − ȳ)sqrt( sum of (xi − x̄)2 · sum of (yi − ȳ)2 )

Five weeks of rain (x) and flow (y)

x = 1, 2, 3, 4, 5
y = 1, 3, 2, 5, 4      mean x = mean y = 3

sum (x-mx)(y-my) = 4+0+0+2+2 = 8
sum (x-mx)² = 10    sum (y-my)² = 10

r = 8 / sqrt(10 × 10) = 0.80
Most points sit upper-right or lower-left of the means, so the products are positive.
Part 1 · Concept 4 of 7

Least Squares

Definition

A residual is observed minus predicted. Least squares picks the line with the smallest sum of squared residuals, SSE.

Equation

ei = yi − yhati
SSE = sum of ei2, made as small as possible

Same five points, line yhat = 0.6 + 0.8x

Residuals:
  −0.4, +0.8, −1.0, +1.2, −0.6

SSE = 0.16+0.64+1.00+1.44+0.36
    = 3.60

Every other straight line gives a larger SSE
Residuals are vertical because we predict y from x. Squaring makes one big miss count more than several small ones.
Part 1 · Concept 5 of 7

Slope and Intercept

Definition

The slope is the change in y for one more unit of x. The intercept is the predicted y at x = 0.

Equation

yhat = b0 + b1x
b1 = sum of (xi − x̄)(yi − ȳ)sum of (xi − x̄)2
b0 = ȳ − b1x̄

Same five points

b1 = 8 / 10 = 0.8
b0 = 3 − 0.8 × 3 = 0.6

If y is log10 flow:
  +0.8 in log10 = ×10^0.8 = ×6.3 in cfs
b1 uses the same sum as r: 8. Dividing by the sum of (x − mean)² instead puts it in units of y per x.
Part 1 · Concept 6 of 7

r², the Share of Spread Explained

Definition

r² is the share of the spread in y that the line accounts for, from 0 to 1.

Equation

r2 = 1 − SSESST
SST = sum of (yi − ȳ)2
SSE = sum of (yi − yhati)2

Same five points

SST = 10.0   misses from the flat mean
SSE =  3.6   misses from the line

r² = 1 − 3.6 / 10
   = 0.64
   = 0.80²
The line removes 6.4 of the 10 units of squared spread.
Part 1 · Concept 7 of 7

RMSE, the Typical Miss

Definition

RMSE, root mean squared error, is the typical size of a residual, in the units of y.

Equation

RMSE = sqrt( (1/n) sum of (yi − yhati)2 )
     = sqrt( SSEn )

Same five points

RMSE = sqrt(3.6 / 5)
     = sqrt(0.72)
     = 0.85

If y is log10 flow:
  ×10^0.85 = about ×7 in cfs
Most points fall within one RMSE of the line. r² is a share; RMSE is a size, in y's units.
Part 1 · Summary

The Seven Measures, Then on the Iowa River

MeasureWhat it tells youFive numbersIowa River, 2019–2025
Mean and medianTypical value; gap means a tail6 and 32,248 and 1,170 cfs
Skew, log10Lopsidedness; log evens it outraw lopsided, log even1.76 raw, −0.02 log10
Correlation, rHow straight, not why0.800.31, 7-day rain vs log10 flow
Slope and interceptEffect per unit; value at zero0.8 and 0.60.0082 per mm and 2.945, in log10
r²Share of spread explained0.640.099
RMSETypical size of a miss0.850.49 log10, a factor of 3.1
2

The Iowa River

The same measures on 2,549 days of rain and flow.

Data

Today's Datasets

File or tableSourceCoverageUsed for
iowa_streamflow_daily.csvUSGS NWIS, site 05454500, Iowa River at Iowa CityDaily, 2015–2025The response: discharge
iowa_city_weather_daily.csvIowa Environmental Mesonet, station IOWDaily, 2019–2025Rain, temperature, dew point
merged, then sThe two files joined on date (Week 6)2,549 days; 2,537 with a full 7-day windowEvery fit today
train / tests split on 2024-01-011,820 days / 717 daysThe honest score
01 · Look at it first

Daily Flow Is Skewed: Most Days Quiet, a Few Floods

Raw units

05,00010,000Daily discharge (cfs)0250500days12

log10 units

1001,00010,000Discharge (cfs, log10 scale)0100200days3
1Median: half the days are lower1,170 cfs
2Mean: pulled up by floods2,248 cfs
3log10 makes it symmetricskew 1.76 → −0.02

68% of days sit below the mean. The wettest 10% of days carry 38% of the water. A straight line fitted in raw units spends its effort on those few days.

Iowa River at Iowa City, 2,549 days with weather, 2019–2025.

01 · Look at it first

Summarize Before You Model

Python

q = sample["discharge_cfs"]
print(q.describe().round(0).to_string())
print(f"skew, raw:   {q.skew():.2f}")
print(f"skew, log10: {np.log10(q).skew():.2f}")

Output

count     2549.0
mean      2248.0
std       2584.0
min         89.0
25%        474.0
50%       1170.0
75%       2900.0
max      12600.0
skew, raw:   1.76
skew, log10: -0.02
  • The standard deviation (2,584) is larger than the mean: a sign of a long tail
  • Skew near 0 means symmetric; the log10 transform takes it from 1.76 to −0.02
Pause and think

?

A report says typical Iowa River flow is 2,248 cfs. Two thirds of days are below that. What should it report instead?

The median, 1,170 cfs: half the days are higher, half lower
The mean is pulled up by a few floods; for a skewed variable, report both
02 · Correlation and its traps

One Number, Three Different Stories

Air temperature and dew point

−20020Dew point (°C)−20020 °CAir temperature
r = 0.97

Shared physics: warm air holds more water. Strong, and no discovery.

Same-day rain and river flow

05k10kDischarge (cfs)04080 mmSame-day rain
r = 0.11

Weak: the river answers rain days later, and most days have no rain.

7-day rain and log10 flow

1001,00010,000Discharge (cfs, log)050100 mm7-day rain
r = 0.31

Same two records, chosen with physics: three times the correlation.

02 · Correlation and its traps

A Third Variable: the Season

024Rain (mm/day)
02,5005,000Flow (cfs)JFMAMJJASOND
1Monthly rain vs monthly flowr = 0.43
2After removing each month's normalr = 0.27

Both records rise in spring and fall in late summer. Part of their correlation comes from that shared calendar, not from one driving the other day by day.

Removing each month's normal (Week 6 anomalies) takes out the season and shrinks r. What is left is closer to the rain-to-river link itself.

Left: mean of each calendar month, 2019–2025, rain at Iowa City (top) and Iowa River discharge (bottom).

Pause and think

?

Ice cream sales and drownings rise and fall together. What drives both, and what plays that role for rain and river flow?

Summer heat drives both: a confounder, the classic third variable
The season lifts rain and flow together; removing it takes monthly r from 0.43 to 0.27
03 · Least squares on our data

Fitting the Line on Our Data

Python

fit = linear_regression_summary(s["precip_7day_mm"],
                                s["log10_discharge"])
for key in ["slope", "intercept", "r_squared",
            "p_value", "stderr", "rmse"]:
    print(f"{key:>10}: {fit[key]:.4g}")

Output

     slope: 0.008213
 intercept: 2.945
 r_squared: 0.09887
   p_value: 2.486e-59
    stderr: 0.0004925
      rmse: 0.4939
  • slope: log10 cfs per mm of 7-day rain; intercept: log10 flow on a dry week
  • r_squared 0.099: the line explains about 10% of the variation in log10 flow
  • rmse 0.49 log10 units: a typical miss is a factor of 3.1 in discharge
Pause and think

?

The slope is 0.0082 log10 cfs per mm. One inch (25.4 mm) of rain falls in a week. By what factor does the line say flow changes?

In log10 units: 0.0082 × 25.4 = 0.21
Back in cfs: 10^0.21 = 1.62, so about 62% more flow than a dry week
03 · Least squares on our data

Reading the Line in Flow Units

Python

a, b = fit["intercept"], fit["slope"]
print(f"dry week:       {10 ** a:,.0f} cfs")
print(f"+25.4 mm rain:  x {10 ** (b * 25.4):.2f}")
print(f"+50 mm rain:    x {10 ** (b * 50):.2f}")
print(f"typical miss:   x {10 ** fit['rmse']:.1f}")

Output

dry week:       881 cfs
+25.4 mm rain:  x 1.62
+50 mm rain:    x 2.57
typical miss:   x 3.1
  • A model fitted in log10 adds in log10, so it multiplies in cfs
  • Each answer is a factor, never "plus so many cfs"
  • The typical miss (×3.1) is larger than the effect of an inch of rain (×1.6)
03 · Least squares on our data

Four Choices, Four r² Values

Same records, four choices of x and y (all years)

Same-day rain, raw flow0.012Same-day rain, log10 flow0.0157-day rain, raw flow0.0807-day rain, log10 flow0.09900.050.100.15r-squared

Longer rain windows, log10 flow (Group Lab 2, Module A)

00.050.100.15r-squared, 2019–20231 d3 d7 d14 d30 d171430Rain window (days)
Choices made from physics raised r² tenfold. It is still 0.10: about 90% of the variation is something rain at one station cannot explain.
04 · Live critique

Find Five Problems

x = merged["precipitation_mm"]
y = merged["discharge_cfs"]
b, a = np.polyfit(x, y, 1)
r2 = np.corrcoef(x, y)[0, 1] ** 2

fig, ax = plt.subplots(figsize=(6, 4))
ax.scatter(x, y)
ax.plot(x, a + b * x, color="tab:orange")
ax.set_title(f"Rain drives river flow (R² = {r2:.2f})")

Same 2,549 days as the rest of the lecture. Work in pairs for two minutes: find five problems, from the data choices to the title.

Rain drives river flow (R² = 0.01, p < 0.001)02040608004000800012000
04 · Live critique

Five Problems, Named

Rain drives river flow (R² = 0.01, p < 0.001)0204060800400080001200012345
1Same-day rain; the river answers days laterrolling("7D")
2Raw flow: a few floods steer the linenp.log10
3A causal title on r² = 0.01"suggests"
4No axis labels, no unitsset_xlabel
5Scored only on the days it was fitted tosplit by time

Problems 1 and 2 are about the science, 3 and 4 about the claim, 5 about the evidence. The next three slides fix them in that order.

04 · Rebuild, step 1 of 3

Give the Model the River's Memory and Scale

s = merged.sort_values("date").set_index("date")
s["precip_7day_mm"] = (s["precipitation_mm"]
                       .rolling("7D", min_periods=7).sum())
s["log10_discharge"] = np.log10(s["discharge_cfs"])
s = (s.dropna(subset=["precip_7day_mm", "log10_discharge"])
      .reset_index())

fit = linear_regression_summary(s["precip_7day_mm"],
                                s["log10_discharge"])

Fixes 1 and 2. Same two files; r² rises from about 0.01 to 0.099. Both choices came from physics before the fit: rivers integrate rain over days, and flow varies by factors.

Fixed: the river's memory and its skew

1001,00010,000Discharge (cfs, log10)r² = 0.099050100150 mm7-day rain
04 · Rebuild, step 2 of 3

Label in Real Units, Title at the Right Strength

fig, ax = plt.subplots(figsize=(8, 5))
ax.scatter(s["precip_7day_mm"], s["discharge_cfs"],
           s=6, alpha=0.15, color="0.2")
xx = np.linspace(0, 150, 100)
ax.plot(xx, 10 ** (fit["intercept"] + fit["slope"] * xx))
ax.set_yscale("log")
ax.set_xlabel("Rain in the previous 7 days (mm)")
ax.set_ylabel("Discharge (cfs)")
ax.set_title("Wetter weeks bring higher flow;\n"
             "rain explains about 10% of the variation")

Fixes 3 and 4. The axis is labelled in cfs on a log scale, which a reader can check against a gauge. The title states the pattern and its size; it claims no cause.

Fixed: units a reader can check, a title that matches r²

Wetter weeks bring higher flow;rain explains about 10% of the variation1001,00010,000050100150Rain in the previous 7 days (mm)Discharge (cfs, log scale)
04 · Rebuild, step 3 of 3

Score on Years the Fit Never Saw

train, test = train_test_split_by_time(s, "date", "2024-01-01")
fit_train = linear_regression_summary(train["precip_7day_mm"],
                                      train["log10_discharge"])
b0, b1 = fit_train["intercept"], fit_train["slope"]
pred = b0 + b1 * test["precip_7day_mm"]
mean_only = train["log10_discharge"].mean()

print(rmse(test["log10_discharge"], pred))       # the model
print(rmse(test["log10_discharge"], mean_only))  # a flat line
AModel, train years 2019–2023RMSE 0.505
BModel, test years 2024–2025RMSE 0.467
CFlat line at the training meanRMSE 0.514

Fixed: evidence from years the fit never saw

1001,00010,000Discharge (cfs, log10)20242025Test years, never seen by the fit

Black: observed. Red: the model. Dashed: the flat line. The model follows the rainy spells but misses the size of most floods and droughts; it beats the flat line by only 0.047 log10 units.

04 · Result

The Rebuilt Result, in a Three-Part Caption

The caption a reader needs, assembled from the three steps.

What

Daily discharge of the Iowa River at Iowa City against rain at Iowa City summed over the previous 7 days, 2,537 days, 2019–2025, log10 flow.

How

Least-squares line fitted on 2019–2023 (1,820 days); scored on 2024–2025 (717 days) that the fit never saw.

Notice

Wetter weeks bring higher flow (slope 0.0066 log10 cfs per mm), but rain explains about 10% of the variation, and the test error is a factor of 2.9 in discharge.

Pause and think

?

The test RMSE (0.467) came out lower than the training RMSE (0.505). Celebrate, or investigate?

Investigate: an error means little until it is set beside something simple
A flat line at the training mean scores 0.514 on the same test years
05 · Residuals

Three Shapes Residuals Take

Random scatter

fitted value

The line took what a line can. The rest is noise or missing variables.

A curve

fitted value

The relationship bends: a straight line is the wrong shape.

A funnel

fitted value

Spread grows with the fitted value. A log transform usually cures it.

Our data, 7-day rain fit: in raw cfs the residual spread grows from 1,469 to 2,997 cfs from the lowest to the highest third of fitted values, a funnel. In log10 units it stays between 0.44 and 0.53.

05 · Residuals

Our Residuals Follow the Seasons

−0.50+0.5Mean residual (log10 cfs)−0.18−0.06+0.29+0.32+0.32+0.42+0.25−0.16−0.47−0.41−0.23−0.12JFMAMJJASOND
+Spring and early summer: more flow than the rain predictsJun +0.42
−Late summer and autumn: lessSep −0.47

+0.42 in log10 units is a factor of 2.6; −0.47 is a factor of 2.9 the other way.

The same week of rain does more in May than in September. Wet soils send rain to the river; dry soils and growing crops keep it. One rain predictor cannot hold that, so the residuals do.

05 · Residuals

The Five Largest Misses: Look Them Up

Date7-day rainObservedLine expectsResidualHypothesis to check
2023-10-1747.8 mm173 cfs2,173 cfs−1.10Dry autumn soils took up the rain
2023-10-1845.5 mm169 cfs2,081 cfs−1.09The same storm, a day later
2023-12-099.1 mm89 cfs1,047 cfs−1.07Lowest flow of 2019–2025
2025-08-070.0 mm10,300 cfs881 cfs+1.07Rain upstream, routed through Coralville Lake
2025-08-080.0 mm10,300 cfs881 cfs+1.07The same flood wave

Investigate, never delete silently: each hypothesis can be checked against another gauge or station.

06 · Honest evaluation

Why Split by Time, Not at Random

1,0003,00010,000Discharge (cfs, log10)AprMayJunJul2019, one dot per day
1Today vs yesterday, log10 flowr = 0.99
2Today vs a week agor = 0.90

Each day is almost a copy of its neighbours. Draw test days at random and nearly every one has its yesterday and tomorrow in the training set: the test is not new data.

Our two-parameter line has little to memorise, so a random split scores about the same (0.503). A flexible model would not; the time split stays honest either way.

06 · Honest evaluation

How Good Is a Test RMSE of 0.467?

Flat line at the training mean0.514Each month's training mean0.495Our 7-day rain model0.467Yesterday's flow0.07100.20.40.6Test RMSE, 2024–2025 (log10 cfs; lower is better)

An error means little until it is set beside something simple. Our model beats the flat line by 0.047 and the monthly normals by 0.028. Yesterday's flow is 7 times better: the river's own memory outpredicts rain. The model describes a relationship; it is not a forecast.

06 · Uncertainty

How Sure Are We of the Slope?

Python

lo = fit["slope"] - 1.96 * fit["stderr"]
hi = fit["slope"] + 1.96 * fit["stderr"]
print(f"slope   {fit['slope']:.4f} log10 cfs per mm")
print(f"95% CI  {lo:.4f} to {hi:.4f}")
print(f"p       {fit['p_value']:.1e}")
print(f"r²      {fit['r_squared']:.3f}")

Output

slope   0.0082 log10 cfs per mm
95% CI  0.0072 to 0.0092
p       2.5e-59
r²      0.099
  • A tiny p and a small r² are both true: a precise estimate of a weak effect
  • The interval assumes independent days; at r = 0.99 day to day they are not
  • So treat 0.0072 to 0.0092 as the narrowest the truth could be, not the range
06 · Calibrated claims

Match the Verb to the Evidence

VerbEvidence it needsOur sentence
suggestsA pattern in one datasetWetter weeks suggest higher flow (r² = 0.10)
showsA check that could have failedThe residuals show a seasonal cycle: +0.42 in June, −0.47 in September
predictsSkill on unseen data, beyond a baselineNot yet: 0.047 better than a flat line, far behind yesterday's flow
provesA mechanism plus a controlled testNothing here proves cause
3

Group Lab 2

Two partners, one honest regression.

07 · This week's lab

How Group Lab 2 Runs

1

Shared start

Both partners, 20 minutes: same-day fit, then 7-day rain against log10 flow; set split_date.

2

Module A

Partner A chooses the predictor: correlation matrix, rain windows of 3, 7 and 14 days.

3

Module B

Partner B tests honestly: residual panels, train and test RMSE with evaluate_window.

4

Joint finish

Run A's window through B's function, plot test residuals by month, cross-check one number each.

Swap letters from Group Lab 1, so each partner leads both kinds of work this semester.

07 · Pitfalls

Where This Week Goes Wrong

  • Slope reported in cfs when it lives in log10 space
  • r² of 0.10 called "useless", or "proof", without a baseline beside it
  • Rain window chosen by peeking at the test years
  • The largest residuals deleted instead of looked up
  • A random train/test split on daily data
Reference

Statistics Functions at a Glance

FunctionReturnsUse it forToday
s.describe()Count, mean, quartilesSummarize before modelingDaily discharge
s.skew()Asymmetry of the distributionWhether to transform1.76 raw, −0.02 log10
np.log10(s)Values by order of magnitudeMultiplicative variableslog10 discharge
s.rolling("7D").sum()A trailing window totalMemory of past days7-day rain
df.corr()Pearson r for every pairScreen candidate predictorsTemperature vs dew point
linear_regression_summary(x, y)Slope, intercept, r², p, stderrFit and read a line7-day rain vs log10 flow
np.polyfit(x, y, 1)Slope and intercept onlyA quick lineThe critique slide
train_test_split_by_time(df, ...)Train before, test after a dateAn honest score2019–2023 / 2024–2025
rmse(y_true, y_pred)Typical miss, in y unitsComparing models0.467 log10 cfs
s.autocorr(1)Correlation with yesterdayHow much a day repeats0.99

linear_regression_summary, rmse and train_test_split_by_time live in src/earthcourse/stats.py.

Summary

Key Takeaways

  • Look at the distribution first; it chooses the units of the model
  • Correlation screens; the season and other third variables can produce it
  • A slope fitted in log10 is a factor in cfs
  • Residual structure is missed science: here, the seasons
  • Score on unseen years, beside a simple baseline
  • Suggests, shows, predicts, proves: pick the verb the evidence earns
Discussion · Next week: algorithm development

?

Yesterday's flow predicts today's seven times better than rain does. Is the rain model useless?

Persistence forecasts; the rain model tests a relationship
Next week you write the tools yourself: smoothing, gap filling, and honest tests of each