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

Algorithm Development

Implement once what you usually import, and every future import becomes an informed choice.

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

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

  • Write pseudocode that names a window's placement and its edge policy
  • Implement a centered moving average and median from that pseudocode
  • Explain how window placement and width move a flood peak
  • Choose an edge policy and state it
  • Verify code with a tiny test, a trusted twin, and manufactured truth
  • Measure how gap-filling error grows with gap length and position
Contents

Today's Lecture

01Hidden decisions
05Verification
02Pseudocode
06Gap filling
03Window placement
07Group Lab 3
04Edges
Recap

Last Week in Sixty Seconds

  • Look at the distribution first; it chooses the units
  • A slope fitted in log10 is a factor in cfs
  • Residuals that follow the seasons are missed science
  • Score on unseen years, beside a simple baseline
  • Today: open the one-line tools those analyses relied on
Data

Today's Dataset

File or valueSourceCoverageUsed for
iowa_streamflow_daily.csvUSGS NWIS, site 05454500, Iowa River at Iowa CityDaily, 2015–2025: 4,018 days, none missingEvery algorithm today
flowstream["discharge_cfs"].to_numpy()One NumPy arrayLoops index it by position
The largest floodnp.argmax(flow)14,800 cfs on 2018-10-06Peak timing, the hardest gap
01

Hidden Decisions

One line of pandas, three decisions you did not make.

01 · Hidden decisions

One Line, Three Hidden Decisions

smooth = stream["discharge_cfs"].rolling(14).mean()
05,00010,00015,000Discharge (cfs)1raw peaksmoothed peakSep 1Oct 1Nov 12018, the largest flood in the record
1Placement: trailing, so the peak arrives late+13 days
2Edges: nothing until 14 days exist13 NaN
3Missing data: one NaN blanks 14 valuesmin_periods

Grey: daily flow. Red: the one-line 14-day mean. Both defaults, center=False and min_periods=14, are decisions someone else made; the output looks like data either way.

Pause and think

?

rolling(14).mean() returned NaN for the first 13 days of the record. Bug, or decision?

A decision: min_periods defaults to the window, so pandas refuses a 14-day mean from fewer days
min_periods=1 makes the opposite choice: report a mean of whatever days exist
02

Pseudocode

Plain language before Python, so the logic is wrong on paper and not in code.

02 · Pseudocode

The Centered Window, Drawn

i = 1clipped at the start01234567891011start = max(0, 1 − 2) = 0; end = min(12, 1 + 2 + 1) = 4; 4 valuesi = 6a full window01234567891011start = max(0, 6 − 2) = 4; end = min(12, 6 + 2 + 1) = 9; 5 valuesi = 11clipped at the end01234567891011start = max(0, 11 − 2) = 9; end = min(12, 11 + 2 + 1) = 12; 3 values

Twelve days, a 5-day window (half = 2). Red: the day being smoothed. Pink: the days averaged for it. Near the ends the window shrinks, so those values rest on fewer days; that shrinking is an edge policy, chosen in the pseudocode.

02 · Pseudocode

The Centered Moving Average, in English

Pseudocode

for each position i in the series:
    start = i minus half the window (not below 0)
    end   = i plus half the window (not past the end)
    output[i] = mean of values from start to end
  • Lines 2 and 3 hold the placement: half the window on each side
  • The two parentheses hold the edge policy: shrink, do not pad
  • Both partners agree on these four lines before anyone types Python
Pause and think

?

Rewrite the pseudocode for a trailing window. Which lines change?

start = i minus (window − 1), not below 0
end = i: the window sees only today and the past
03

Window Placement

From English to a loop, and why the timing of a window is science.

03 · Window placement

From Pseudocode to Python

Python

def moving_average_centered(values, window):
    half = window // 2
    out = []
    for i in range(len(values)):
        start = max(0, i - half)
        end = min(len(values), i + half + 1)
        out.append(np.mean(values[start:end]))
    return np.array(out)

print(moving_average_centered(np.array([1., 2., 3., 4., 5.]), 3))

Output

[1.5 2.  3.  4.  4.5]
  • max and min are the edge policy from the pseudocode, written down
  • The + 1 is there because a slice stops before its end
  • Five numbers you can check by hand: the middle must be 3.0
03 · Window placement

Trailing vs Centered on the 2018 Flood

05,00010,00015,000Discharge (cfs)1234Sep 1Oct 1Nov 12018
1Raw daily flow14,800 · Oct 6
27-day centered mean13,043 · +3 d
37-day centered median13,000 · +2 d
414-day trailing mean12,743 · +13 d

Grey: daily. Red: centered mean. Black: centered median. Dashed: trailing mean. The flood is a one-day spike on a long plateau: the next two weeks stay near 12,500 cfs, while eight days earlier the river ran at 1,490. Even a symmetric window leans toward the plateau.

Pause and think

?

Your 14-day trailing mean puts the 2018 flood peak 13 days late. Bug, or design decision? Defend both answers.

A bug, if a report reads flood timing from the smoothed series
A decision, if the system can only see the past, as in real-time monitoring
03 · Window placement

Wider Windows Keep Less

7-day window

2018201910,000 cfs
spread kept 97%

Storms and floods survive.

30-day window

2018201910,000 cfs
spread kept 89%

Single storms blur; wet months stay.

90-day window

2018201910,000 cfs
spread kept 74%

Floods merge into seasons.

365-day window

2018201910,000 cfs
spread kept 51%

Only wet and dry years remain.

Centered means over the whole 2015–2025 record, shown for 2018–2019. "Spread kept" is the standard deviation of the smoothed series as a share of the raw one. Pick the width from the signal the question is about.

04

Edges

Every window meets the end of the record; the policy there is a choice.

04 · Edges

Three Tools, Three Edge Policies

05001,0001,5002,000Discharge (cfs)123Jan 1Jan 15Feb 1The first days of the record, 2015
1Refuse: rolling(15, center=True)7 NaN each end
2Shrink: min_periods=1day 1 uses 8 days
3Pad with zeros: np.convolve(..., "same")642 vs 1,050 cfs

Dots: daily flow. One 15-day centered mean, three answers at the edges. Away from the edges all three agree to within 4e-12 cfs. Zero padding pulls the first week toward a dry river that never existed.

Pause and think

?

How many values of a 14-day trailing mean rest on fewer than 14 days? When does that matter?

13 at the start: 0.3% of a 4,018-day record, easy to ignore
Almost half of a 30-day field campaign: state the policy in the caption
05

Verification

A draft with four problems, and the three checks that catch them.

05 · Verification

Three Rungs of Verification

1

Tiny input

Five numbers with a hand-checked answer: the middle of [1..5] with window 3 is 3.0.

2

Trusted twin

Agreement with an independent implementation, pandas, on all 4,018 days.

3

Manufactured truth

Hide data you have, rebuild it, score the result: the gap experiment.

Each rung catches what the one below misses. The next five slides climb the first two.

05 · Live critique

Find Four Problems

def moving_average_draft(values, window):
    half = window // 2
    out = np.zeros(len(values))
    for i in range(half, len(values) - half):
        out[i] = values[i - half:i + half].mean()
    return out

smooth = moving_average_draft(flow, 7)
plt.plot(stream["date"], flow, color="0.8")
plt.plot(stream["date"], smooth)

It runs, and its line hugs pandas (r = 0.9986). Work in pairs for two minutes: find four problems in the code, not in the plot.

Red: the draft. Black: pandas, centered, 7 days.

05,00010,00015,000Discharge (cfs)JanAprJulOct2018
05 · Live critique

Four Problems, Named

def moving_average_draft(values, window):
    half = window // 2                         # 3
    out = np.zeros(len(values))                # 2
    for i in range(half, len(values) - half):  # 2
        out[i] = values[i - half:i + half].mean()  # 1
    return out
                                               # 4

Problems 1 and 3 are arithmetic; 2 is an edge policy nobody chose; 4 is why the other three survived. The largest error inside the record is 1,133 cfs, and 6 edge days read 0 cfs.

1Slice drops the last day: 6 values in a 7-day window, half a day early+ 1
2Edge days left at 0 cfs, which reads as a dry rivershrink or NaN
3Any window accepted; an even one has no middle dayValueError
4Checked by eye on a 4,018-day plotassert
05 · Verification

The Tiny Test Catches It

Python

check = moving_average_draft(np.array([1., 2., 3., 4., 5.]), 3)
print(check)
assert check[2] == 3.0, f"middle value is {check[2]}, not 3.0"

Output

[0.  1.5 2.5 3.5 0. ]
AssertionError: middle value is 2.5, not 3.0
  • Three lines expose problems 1 and 2 at once: 2.5 instead of 3, and zeros at the ends
  • An assert stops the notebook, so a broken tool cannot feed a figure
  • The expected value came from the pseudocode, not from running the code
05 · Verification

A Trusted Twin on 4,018 Days

Python

mine = moving_average_centered(flow, 7)
trusted = (stream["discharge_cfs"]
           .rolling(7, center=True, min_periods=1)
           .mean().to_numpy())
print("largest difference:", np.max(np.abs(mine - trusted)), "cfs")
print("agree:", np.allclose(mine, trusted))

Output

largest difference: 1.4210854715202004e-14 cfs
agree: True
  • Two independent implementations agree to 1e-14 cfs, rounding error
  • min_periods=1 makes pandas shrink at the edges, as our loop does
  • Agreement is evidence because the two were written separately
05 · Verification

An Even Window Breaks the Twin

Python

mine = moving_average_centered(flow, 14)
trusted = (stream["discharge_cfs"]
           .rolling(14, center=True, min_periods=1)
           .mean().to_numpy())
print("largest difference:", round(np.max(np.abs(mine - trusted))), "cfs")
print("values in my window:", 2 * (14 // 2) + 1)
print(pd.Series(range(6)).rolling(4, center=True).mean().tolist())

Output

largest difference: 508 cfs
values in my window: 15
[nan, nan, 1.5, 2.5, 3.5, nan]
  • A 14-day window has no middle day: the loop silently uses 15 values
  • pandas puts 7 days before and 6 after; the last line shows its rule on 6 numbers
  • Problem 3 fixed: raise ValueError for an even window, as the lab requires
05 · Verification

Faster Code Needs the Same Checks

Python

import timeit
q = stream["discharge_cfs"]
tasks = {
    "loop": lambda: moving_average_centered(flow, 15),
    "pandas": lambda: q.rolling(15, center=True, min_periods=1).mean(),
    "convolve": lambda: np.convolve(flow, np.ones(15) / 15, mode="same"),
}
for name, task in tasks.items():
    ms = 1000 * min(timeit.repeat(task, number=1, repeat=5))
    print(f"{name:>8}: {ms:6.2f} ms")

Output

    loop:  12.73 ms
  pandas:   0.11 ms
convolve:   0.14 ms
  • About 100 times faster, and 13 ms is still fine for 4,018 days
  • np.convolve is fast and pads with zeros: speed changed the edge policy
  • Make it right, then make it fast, then run the tests again
Pause and think

?

What can a tiny test catch that agreement with pandas cannot?

A shared mistake: trailing when the science needed centered still agrees with trailing pandas
What you meant: the middle of [1..5] must be 3, whatever any library does
06

Gap Filling

Hide data we have, rebuild it, and score the rebuild against the truth.

06 · Gap filling

Fill a Gap, Score It Against the Truth

3-day gap

Jun 8Aug 6
RMSE 48 cfs

7-day gap

Jun 8Aug 6
RMSE 218 cfs

14-day gap

Jun 8Aug 6
RMSE 943 cfs

30-day gap

Jun 8Aug 6
RMSE 3,331 cfs

One position, 2020-06-23, 2020. Black: the true flow we hid. Red: the straight line from the last good day to the next one. Pink: the gap. Because we made the gap, we know the truth and can score the fill; with a real outage the truth is gone.

06 · Gap filling

One Position Can Be Lucky: Test Twenty

1101001,000RMSE of the fill (cfs, log scale)meanmedian371430Gap length (days)
13-day gaps123 (1–409)
27-day gaps292 (4–1,500)
314-day gaps405 (10–1,671)
430-day gaps884 (22–2,348)

Mean RMSE in cfs, with the range over 20 evenly spaced positions. Dots: each position. Pink: lowest to highest. The mean sits above the median at every length: most gaps are easy, and a few hide a storm.

06 · Gap filling

Straight Line or Last Week's Mean?

Python

line, last_week = [], []
for s0 in np.linspace(31, len(flow) - 61, 20).astype(int):
    hidden = flow[s0:s0 + 7]
    with_gap = flow.copy()
    with_gap[s0:s0 + 7] = np.nan
    line.append(rmse(hidden, fill_gap_linear(with_gap, s0, 7)[s0:s0 + 7]))
    last_week.append(rmse(hidden, np.full(7, flow[s0 - 7:s0].mean())))
line, last_week = np.array(line), np.array(last_week)
print(f"straight line:    {line.mean():4.0f} cfs")
print(f"last week's mean: {last_week.mean():4.0f} cfs")
print(f"the line wins at {(line < last_week).sum()} of 20 positions")

Output

straight line:     292 cfs
last week's mean:  426 cfs
the line wins at 15 of 20 positions
  • Same 20 positions, two methods: a fair comparison
  • The line also sees the day after the gap; last week's mean cannot
  • It loses at 5 positions: no fill method wins everywhere
06 · Gap filling

The Hardest Place: Just Before the Flood

05,00010,00015,000Discharge (cfs)123Sep 15Oct 1Oct 15Nov 12018
1The gap hides the rise1,490 → 10,300
2Straight-line fill misses its shapeRMSE 2,633
3A 7-day median then lowers the peak14,800 → 13,000

A 7-day gap, 09-28 to 10-04, ending two days before the peak. Its error beats the worst 30-day gap of the random test (2,348 cfs): position matters as much as length. Flag filled values as estimated.

Pause and think

?

The mean error of a 30-day gap is seven times that of a 3-day gap. What does that tell a sensor-network manager?

A typical filled day is off by about 884 cfs after a month offline, 123 after three days
Repair before storm season, and flag every filled value as estimated
07

Group Lab 3

Two partners, two algorithms, one flood.

07 · This week's lab

How Group Lab 3 Runs

1

Shared start

Both partners, 15 minutes: load flow, write the trailing moving average, check it against pandas.

2

Module A

Partner A smooths without shifting the peak: moving_median_centered and peak_table.

3

Module B

Partner B fills sensor gaps: gap_error_table for 3- to 30-day gaps at 20 positions.

4

Joint finish

Fill a gap before the largest flood, smooth it, run each other's tiny test, explain.

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

07 · Pitfalls

Where This Week Goes Wrong

  • The missing + 1 at the window's end
  • An even window passed to a "centered" function
  • Edge values read as if a full window supported them
  • A gap tested at one lucky position only
  • Filled values reported as data instead of flagged as estimated
Reference

Smoothing and Filling Functions at a Glance

FunctionDoesUse it forToday
s.rolling(w).mean()Trailing mean; NaN until w valuesReal-time smoothingPeak +13 days
s.rolling(w, center=True)Window on both sidesFinished recordsPeak +3 days
min_periods=1Shrink the window at the edgesReport every dayFirst value from 8 days
s.rolling(w, ...).median()Middle value of each windowSpiky records13,000 cfs peak
np.convolve(x, k, "same")Weighted sum; zeros past the edgesFast, any kernel642 cfs on day 1
s.interpolate()Straight line across NaNShort sensor gapsMatches fill_gap_linear
np.argmax(x)Position of the largest valueFind the flood2018-10-06
assert cond, msgStop when cond is FalseTiny tests[1..5] middle is 3
np.allclose(a, b)All equal within a toleranceTrusted-twin checks1e-14 cfs apart
rmse(y_true, y_pred)Typical miss, in y unitsScoring a fill884 cfs, 30 days

rmse lives in src/earthcourse/stats.py; the loops you write in the lab sit beside these as their twins.

Summary

Key Takeaways

  • One line of pandas hides placement, edges, and a missing-data policy
  • Pseudocode shows those decisions while they are cheap to change
  • Trailing windows delay peaks; centered windows can shift an uneven flood
  • Every edge policy is defensible; an unstated one is not
  • Verify three ways: tiny input, trusted twin, manufactured truth
  • Straight-line fills fail where it matters most: inside storms
Discussion · Next week: AI-assisted scientific coding

?

Name one tool you have imported this semester whose hidden decisions you could now list. What are they?

For example, rolling("7D").sum() in Week 8: a trailing window, and min_periods=7 so no sum from fewer days
Next week an AI agent writes the code; the same three checks decide whether to trust it