The brief
A flight school at Santa Barbara (SBA) sends student pilots on solo flights. Its safety rules for a student solo, as far as wind goes:
"The crosswind component on the runway in use must not exceed 8 knots. Calm is fine. Students only use runway 7/25 (the long runway); the tower gives them the end that faces into the wind, so tailwind is not an issue. First-solo students have a stricter 6-knot limit."
The chief instructor asks:
"By month and hour of the day, what is the probability that a solo slot is flyable? We want a default two-hour solo window between 08:00 and 18:00 local time. Which one should it be, how sure are you, and how many bookings a month should we expect to cancel because of the wind? Does the answer change for first-solo students?"
You have three years of hourly wind reports from the airport's own weather station. The obvious approach - fit a model for the wind speed, fit a model for the wind direction, and combine them - gives a confident, plausible, wrong answer. Find out why, fix it, and then check your probabilities on a year the model has not seen.
This is the applied sequel to example E81 (circular data). Do E81 first: this challenge assumes you know the mean resultant vector, the canonical (natural-parameter) von Mises link, circular predictors as harmonics, and what the Santa Barbara wind does over a day. E81 modelled the direction; here the question is about a vector - speed and direction together - and about a small tail probability.
How this notebook works
- Each task states what to deliver, not how. Write your code in the
YOUR CODE HEREcells. - Stuck?
h.hint("task2")reveals hints one level at a time: nudge → approach → code skeleton. Try to get by on nudges. h.check("task2", ...)compares your numbers with the reference solution.- A full worked solution lives in
notebooks/solutions/. Open it only when you are done (or truly stuck).
import logging
import time
import arviz as az
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import pymc as pm
import pytensor.tensor as pt
from scipy import special, stats
from pymc_challenges import Hints, data
RANDOM_SEED = 1932
rng = np.random.default_rng(RANDOM_SEED)
az.style.use("arviz-variat")
logging.getLogger("pymc").setLevel(logging.WARNING) # several fits: no sampler banner per fit
pd.set_option("display.width", 160)
pd.set_option("display.max_columns", 30)
T_START = time.time()
h = Hints("C12")
h.tasks()
C12 - Crosswinds at Santa Barbara: when can a student fly solo?
task0- Look at the data (2 hints)task1- The crosswind, and what the raw data can say (3 hints, 2 check(s))task2- The obvious model, speed and direction separately (3 hints, 2 check(s))task3- Let the direction depend on the speed (3 hints, 2 check(s))task4- Is the model honest about 2023? (3 hints, 3 check(s))task5- The schedule (3 hints, 4 check(s))
The data
Routine hourly reports (METARs), issued at minute 53 of each hour. drct is the direction the
wind blows from, in degrees clockwise from true north, reported in steps of 10; sknt is
the speed in knots. A calm is reported as direction 0 and speed 0. An empty direction with a
non-zero speed is a variable wind (no steady direction).
The school books slots in local clock time, so we convert the UTC timestamps to
America/Los_Angeles (daylight-saving time included). We fit on 2021-2022 and keep
2023 aside: nothing you do before Task 4 may look at it.
data.describe("ksba_wind")
raw = data.load("ksba_wind")
local = pd.to_datetime(raw.valid).dt.tz_localize("UTC").dt.tz_convert("America/Los_Angeles")
wind = raw.assign(hour=local.dt.hour, month=local.dt.month, year=local.dt.year, date=local.dt.date)
wind = wind[(wind.year >= 2021) & wind.sknt.notna()].reset_index(drop=True) # 8 reports fall on 31 Dec 2020 local
train = wind[wind.year < 2023].reset_index(drop=True)
test = wind[wind.year == 2023].reset_index(drop=True)
print(f"training reports (2021-2022): {len(train):,}; held-out 2023: {len(test):,}")
train.head()
ksba_wind: Routine hourly METAR reports, 2021-01-01 to 2023-12-31 (26,101 rows, at minute 53 of each hour): valid (UTC timestamp), drct (direction the wind blows FROM, degrees clockwise from true north, reported in steps of 10; 0 together with sknt = 0 means calm), sknt (speed, knots), tmpf and dwpf (air and dew-point temperature, Fahrenheit). Empty = missing (e.g. variable direction). source: Iowa Environmental Mesonet (IEM, Iowa State University) ASOS/METAR archive, station SBA (Santa Barbara Municipal Airport, California; an FAA/NWS Automated Surface Observing System). Public US government observations redistributed by IEM, which asks to be acknowledged. BUILT by tools/build_e81_circular.py (the URL returns an uncompressed CSV) - keep the cached file. url: https://mesonet.agron.iastate.edu/cgi-bin/request/asos.py?station=SBA&data=drct&data=sknt&data=tmpf&data=dwpf&year1=2021&month1=1&day1=1&year2=2024&month2=1&day2=1&tz=Etc/UTC&format=onlycomma&latlon=no&missing=empty&trace=empty&direct=no&report_type=3 training reports (2021-2022): 17,354; held-out 2023: 8,677
| valid | drct | sknt | tmpf | dwpf | hour | month | year | date | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | 2021-01-01 08:53 | 0.0 | 0.0 | 47.0 | 32.0 | 0 | 1 | 2021 | 2021-01-01 |
| 1 | 2021-01-01 09:53 | 0.0 | 0.0 | 45.0 | 33.0 | 1 | 1 | 2021 | 2021-01-01 |
| 2 | 2021-01-01 10:53 | 0.0 | 0.0 | 41.0 | 32.0 | 2 | 1 | 2021 | 2021-01-01 |
| 3 | 2021-01-01 11:53 | 70.0 | 3.0 | 42.0 | 33.0 | 3 | 1 | 2021 | 2021-01-01 |
| 4 | 2021-01-01 12:53 | 0.0 | 0.0 | 39.0 | 32.0 | 4 | 1 | 2021 | 2021-01-01 |
Runway 7/25, from the FAA airport record (form 5010, as listed e.g. on AirNav): 6,052 ft long, magnetic headings 075° / 255°, true headings 089° / 269°. (Runways are named after their magnetic heading rounded to 10°; the local magnetic variation is about 12-14° east.)
RWY_MAGNETIC = 75.0
RWY_TRUE = 89.0
LIMIT = 8.0 # knots of crosswind, solo students
LIMIT_FIRST = 6.0 # knots of crosswind, first-solo students
DAY_HOURS = range(8, 18) # the school is open 08:00-18:00
Task 0 · Look at the data
Deliver
- How many reports are calm, how many have no direction, and how fast is the wind when the
direction is missing? What are the possible values of
drctandsknt? - Rose diagrams of the afternoon wind (12:00-16:00), split by speed class (light, moderate, strong). Does the direction depend on the speed?
Task 1 · The crosswind, and what the raw data can say
Deliver
- For every report, the crosswind component on runway 7/25 and whether the slot is flyable under the 8-knot rule. Decide what calms and missing directions mean. Why does the choice of runway end not matter for the crosswind?
- The share of 2021-2022 reports that exceed the limit, with the true heading - and with the magnetic heading 075° used by mistake. Which error does the mistake make, and how big is it?
- A month x hour table of the observed exceedance rate. Is it good enough to schedule from?
Task 2 · The obvious model: speed and direction separately
A crosswind exceedance needs a strong wind and the wrong direction. The natural plan is to model each and combine them.
Deliver
- A model for the wind speed as a smooth function of hour and month that treats the calms honestly (they are speeds below a threshold, not a separate kind of wind) and respects that speeds are reported in whole knots. Check it on the share of calms and of strong winds by hour.
- A model for the reported direction as a smooth function of hour and month. Make it flexible enough to have more than one preferred direction (E81 showed that one von Mises is not).
- Combine them into the probability of exceeding 8 knots of crosswind for every month x hour, and compare it, hour by hour, with the observed rate. What goes wrong, and why?
Task 3 · Let the direction depend on the speed
Deliver
- A direction model that conditions on the reported speed as well as on hour and month. Try the canonical von Mises of E81 first, then a more flexible circular family. Combined with your speed model, this is a model of the full wind vector.
- The exceedance check of Task 2, by hour and by month, for both. Which one can you use?
- Show where the inadequate one goes wrong: compare the predicted and observed directions of strong afternoon winds.
Task 4 · Is the model honest about 2023?
The posterior bands in Task 3 are narrow. But the model treats 17,000 hourly reports as independent pieces of evidence, and wind is not like that.
Deliver
- For each month of 2023, the observed share of daytime (08:00-17:59) reports above the limit, and your model's 90% predictive interval for it. How many of the 12 months fall inside?
- Evidence from 2021-2022 that bad hours are clustered (compare what the model expects with what happened at the level of days).
- A model extension that represents the clustering, refitted on 2021-2022 only, and the same 2023 check.
Task 5 · The schedule
A solo slot is a two-hour window $[h, h+2)$; call it flyable if both reports made inside it (at $h$:53 and $h+1$:53) are within the limit.
Deliver
- For every month and every window starting between 08:00 and 16:00, the probability that the window is flyable under the 8-knot rule, from your final model (with its uncertainty).
- The recommended default window for the whole year: its average probability, the posterior probability that it is the best window, and how much better it is than the runner-up and the worst. Would a month-specific window be worth the complication?
- For a student with 20 solo bookings in March in the default window, the expected number cancelled because of the wind, with a 90% predictive interval.
- The same window ranking for first-solo students (6-knot limit). Does the advice change?
Going further
- Gusts. The METAR also reports gusts (not in this extract). Download them from the IEM archive, add a gust model conditional on the mean speed and direction, and redo Task 5 with a gust crosswind limit. How much of the morning advantage survives?
- Persistence. Replace the day effect with a latent AR(1) process over hours (or an HMM over weather regimes) and check whether "the next two hours" can be forecast from the last report - a much more useful product for a dispatcher than climatology.
- Other runway. Runway 15/33 (true headings 166/346) is shorter but nearly across 7/25. Under a rule "either runway", how much flyable time does the school gain, and which wind regimes does the second runway rescue?
- Continuous direction. Replace the 36-bin categorical with a continuous log-Fourier density plus a rounding model (E81 handled rounding by jittering). Do the tail probabilities change?
h.progress()
C12: 0/17 hints used, 13/13 checks passed.
Reference solution
A complete, executed solution with commentary on every task — including the ways the obvious model fails. Try the hint ladder first.
Open the worked solution →