Assignment 4: An h-step forecaster for a plant channel#
Module: Week 04 · Released: L8 (2026-09-21) · Due: 2026-09-28 · 6 points on Canvas.
Goal#
Forecast the stripper temperature of the Tennessee Eastman plant at five horizons, from 3 minutes to 2 hours, and show honestly whether your model is worth using. “Worth using” has a precise meaning here: better than the two free forecasts, persistence and the mean, on simulation runs the model never saw.
Everything you need is in Lecture 8. The demo notebook does the same steps on reactor pressure; this assignment does them on a different channel, with a validation step the demo skips.
You should be able to:
build a forecasting table whose features are known at time
tand whose target isy[t+h], inside each run;score persistence and the mean at each horizon, and compute a skill score against the better one;
fit a direct forecaster per horizon and a recursive one, and compare them;
choose hyperparameters on validation runs, never on test runs;
show how much a shuffled split flatters a model on a single series, using
TimeSeriesSplitwith a gap.
The data#
The fault-free training file from Rieth et al. (2017), the same simulations Lecture 8 uses. The course hosts it as Parquet, 25 MB, so you need no R reader:
mkdir -p data && cd data
curl -fLO https://kitchin-services.cheme.cmu.edu/f26-06763/data/tep_fault_free_training.parquet
curl -fLO https://kitchin-services.cheme.cmu.edu/f26-06763/data/SHA256SUMS
shasum -a 256 --ignore-missing -c SHA256SUMS
pl.read_parquet opens it directly. The original is the TEP_FaultFree_Training.RData file in the Dataverse record above, converted with no other change. Do not commit the data.
Runs |
500 independent simulations, |
Samples |
500 per run, |
Target |
|
Inputs you may use |
the target’s own past, and the 11 valve positions |
Fixed choices#
These are fixed so that every submission can be checked against the same numbers.
Choice |
Value |
|---|---|
Horizons |
1, 5, 10, 20, 40 samples (3, 15, 30, 60, 120 minutes) |
Training runs |
1 to 300 |
Validation runs |
301 to 400, for choosing hyperparameters only |
Test runs |
401 to 500, touched once, at the end |
Forecast origins |
every |
Mean baseline |
the mean of |
Persistence |
|
Error |
RMSE in °C |
Starting every origin at t = 40 means every model and every baseline is scored on exactly the same rows, whatever lag depth you pick, as long as it is 40 or fewer.
Tasks#
1. Build the forecasting table#
Write one function that returns, for a given h and lag depth, one row per (run, t) with the features known at t and the target y[t+h]. Every shift must run inside a run (.over("simulationRun") in Polars, or a per-run loop). Drop rows whose lags or target fall outside their run. No feature may come from after t.
2. Score the baselines#
On the test origins, compute persistence and mean RMSE at every horizon.
3. Choose hyperparameters on the validation runs#
Fit a scaled ridge regression (make_pipeline(StandardScaler(), Ridge(alpha=...))) on the training runs at h = 10, and score it on the validation runs for at least three settings: vary the lag depth, and the penalty if you like. Write every setting you tried to results/validation.csv. Use the best setting for everything that follows. The test runs play no part in this choice.
4. Fit direct and recursive forecasters#
Direct: one model per horizon, trained on runs 1 to 300 with target
y[t+h]. Lags ofxmeas_18plus the valves att.Recursive: one model for
y[t+1]from lags ofxmeas_18only, appliedhtimes, feeding each prediction back into the lags. Say in your report why this one does not use the valves.
You may add a second direct model of your own choice (gradient boosting, a random forest, extra window features), as long as the ridge results are also reported.
5. Save predictions and the results table#
Write results/predictions.parquet with one row per test origin and horizon:
Column |
Type |
Meaning |
|---|---|---|
|
int |
|
|
int |
the forecast origin, |
|
int |
horizon in samples |
|
float |
|
|
float |
your persistence forecast |
|
float |
your mean forecast |
|
float |
your direct forecast |
|
float |
your recursive forecast |
Write results/forecast.csv with one row per horizon and at least these columns: h, persistence_rmse, mean_rmse, direct_rmse, recursive_rmse, skill, where skill is 1 - direct_rmse / min(persistence_rmse, mean_rmse).
6. Shuffle, then don’t#
Take runs 1 to 10, one run at a time, at h = 10. For each run, score a random forest (or another flexible model) two ways, with the same features as your direct model:
KFold(n_splits=5, shuffle=True, random_state=0)TimeSeriesSplit(n_splits=5, gap=10)
Score persistence on the same test folds. Write results/leaky.csv with columns run, split (shuffled or time), h, model_rmse, persistence_rmse, one row per run and split.
7. Write REPORT.md#
Two pages maximum, six sections in this order:
The table. What a row holds, what is known at
t, and how you kept each shift inside its run.Baselines and skill. Your
forecast.csvas a table. Where persistence stops beating the mean, and whether that matches the autocorrelation ofxmeas_18.Validation. What you tried, what you chose, and how much it mattered.
Direct against recursive. Which held up better, where, and why. Why the recursive model has no valves.
Shuffle against time. Your mean
leaky.csvnumbers, which one you would report to a plant manager, and why the gap appears.Limits. One situation where this forecaster would give a confident wrong answer in a real plant.
End with one line disclosing generative-AI use.
Names to use#
Thing |
Name |
|---|---|
Code |
any |
Data |
|
Validation sweep |
|
Predictions |
|
Results table |
|
Shuffle comparison |
|
Report |
|
Submit#
From your project root, after running your code:
uv run --no-project https://kitchingroup.cheme.cmu.edu/f26-06763/a04-evidence.py \
--andrew-id yourid --name "Your Name"
Upload the resulting evidence.pdf to Canvas.
Keep
--no-project, so a brokenuv.lockin your project cannot stop the script.The script reads the Rieth file from your
data/directory (pass--dataif it is elsewhere) and recomputes the baselines and the targets itself. It does not re-run your code, and it does not download anything.Read the PDF before uploading. It prints your score by group. If a check fails, fix it and rerun, and submit anyway if you run out of time.
The PDF prints the script’s sha256, which matches https://kitchingroup.cheme.cmu.edu/f26-06763/a04-evidence.py.sha256 when run from the URL.
Grading#
Five groups are scored by the script and the sixth by your TA. Within a group, checks are equally weighted.
# |
Group |
Pts |
Checks |
|---|---|---|---|
1 |
The table |
1.0 |
|
2 |
Baselines |
1.0 |
|
3 |
Models |
1.25 |
direct and recursive RMSE recomputed from your predictions match |
4 |
Honest evaluation |
1.25 |
|
5 |
Report numbers |
0.5 |
the RMSE values quoted in |
6 |
1.0 |
the six sections and the AI-use line, read by your TA |
A check the script cannot decide is held for your TA rather than lost. Your TA can adjust in either direction when the work is clearly better or worse than the checks show.
AI use#
Generative AI is allowed with disclosure in REPORT.md. Editing the generated PDF by hand is falsifying a submission. You must be able to explain your table, why the gap has to be at least h, and why your shuffled score is lower.
Stretch (not graded)#
A residual detector. Download the faulty training file (datafile
3031242, about 500 MB). Set a threshold on your one-step residuals from validation runs, then run it on fault 1 and one other fault of your choice. Report the false-alarm rate on the test runs and the detection delay on each fault. Faults are introduced one hour into each faulty training run.Plot the autocorrelation of
xmeas_18and predict the persistence-mean crossover from it before looking at your table.Try
skforecast’sForecasterDirectandForecasterRecursiveand check they reproduce your numbers.