LeetLLM
My PlanLearnGlossaryTracksPracticeBlog
LeetLLM

Your go-to resource for mastering AI & LLM systems.

Product

  • Learn
  • Glossary
  • Tracks
  • Practice
  • Blog
  • RSS

Legal

  • Terms of Service
  • Privacy Policy

ยฉ 2026 LeetLLM. All rights reserved.

All Topics
Your Progress
0%

0 of 178 articles completed

๐Ÿ› ๏ธComputing Foundations0/9
Git, Shell, Linux for AIDocker for Reproducible AIPython for AI EngineeringNumPy and Tensor ShapesCUDA for ML TrainingMPS & Metal for ML on MacData Structures for AISQL and Data ModelingAlgorithms for ML Engineers
๐Ÿ“ŠMath & Statistics0/8
Gradients and BackpropVectors, Matrices & TensorsLinear Algebra for MLAdam, Momentum, SchedulersProbability for Machine LearningStatistics and UncertaintyDistributions and SamplingHypothesis Tests, Intervals, and pass@k
๐Ÿ“šPreparation & Prerequisites0/13
Neural Networks from ScratchCNNs from ScratchTraining & BackpropagationSoftmax, Cross-Entropy & OptimizationRNNs, LSTMs, GRUs, and Sequence ModelingAutoencoders and VAEsThe Transformer Architecture End-to-EndLanguage Modeling & Next TokensFrom GPT to Modern LLMsPrompt Engineering FundamentalsCalling LLM APIs in ProductionFirst AI App End-to-EndThe LLM Lifecycle
๐ŸงฎML Algorithms & Evaluation0/11
Linear Regression from ScratchLogistic Regression and MetricsDecision Trees, Forests, and BoostingReinforcement Learning BasicsValidation and LeakageClustering and PCACore Retrieval AlgorithmsDecoding AlgorithmsExperiment Design and A/B TestingPyTorch Training LoopsDataset Pipelines and Data Quality
๐Ÿ“ฆProduction ML Systems0/6
Feature Engineering for Production MLBatch and Streaming Feature PipelinesGradient Boosted Trees in ProductionRanking and Recommendation SystemsForecasting and Anomaly DetectionMonitoring Predictive Models
๐ŸงชCore LLM Foundations0/8
The Bitter Lesson & ComputeBPE, WordPiece, and SentencePieceStatic to Contextual EmbeddingsPerplexity & Model EvaluationFile Ingestion for AIChunking StrategiesLLM Benchmarks & LimitationsInstruction Tuning & Chat Templates
๐ŸงฐApplied LLM Engineering0/24
Dimensionality Reduction for EmbeddingsCoT, ToT & Self-Consistency PromptingFunction Calling & Tool UseMCP & Tool Protocol StandardsContext EngineeringPrompt Injection DefenseResponsible AI GovernanceData Labeling and Human FeedbackEvaluating AI AgentsProduction RAG PipelinesHybrid Search: Dense + SparseReranking and Cross-Encoders for RAGRAG Evaluation for Reliable AnswersLLM-as-a-Judge EvaluationBias & Fairness in LLMsHallucination Detection & MitigationLLM Observability & MonitoringExperiment Tracking with MLflow and W&BPrompt Optimization with DSPyModel Versioning & DeploymentSemantic Caching & Cost OptimizationLLM Cost Engineering & Token EconomicsModel Gateways, Routing, and FallbacksDesign an Automated Support Agent
๐ŸŽ“Portfolio Capstones0/9
Capstone: Delivery ETA PredictionCapstone: Product RankingCapstone: Demand ForecastingCapstone: Image Damage ClassifierCapstone: Production ML PipelineCapstone: Document QACapstone: Eval DashboardCapstone: Fine-Tuned ClassifierCapstone: Reproducible ML Study
๐Ÿง Transformer Deep Dives0/8
Sentence Embeddings & Contrastive LossEmbedding Similarity & QuantizationScaled Dot-Product AttentionVision Transformers and Image EncodersPositional Encoding: RoPE & ALiBiLayer Normalization: Pre-LN vs Post-LNMechanistic InterpretabilityDecoding Strategies: Greedy to Nucleus
๐ŸงฌAdvanced Training & Adaptation0/17
Scaling Laws & Compute-Optimal TrainingPre-training Data at ScaleBuild GPT from Scratch LabJAX for PyTorch ResearchersContinued Pretraining for Domain ShiftSynthetic Data PipelinesSupervised Fine-Tuning PipelineMixed Precision TrainingDistributed Training: FSDP & ZeROLoRA & Parameter-Efficient TuningTraining Run OperationsReward Modeling from Preference DataRLHF & DPO AlignmentConstitutional AI & Red TeamingRLVR & Verifiable RewardsKnowledge Distillation for LLMsModel Merging and Weight Interpolation
๐Ÿค–Advanced Agents & Retrieval0/16
Vector DB Internals: HNSW & IVFAdvanced RAG: HyDE & Self-RAGGraphRAG & Knowledge GraphsRAG Security & Access ControlStructured Output GenerationReAct & Plan-and-ExecuteGuardrails & Safety FiltersCode Generation & SandboxingComputer-Use / GUI / Browser AgentsHuman-in-the-Loop Agent ArchitectureAI Coding Workflow with AgentsAgent Memory & PersistenceAgent Failure & RecoveryRecursive Language Models (RLM)Multi-Agent OrchestrationCapstone: Production Agent
โšกInference & Production Scale0/19
Inference: TTFT, TPS & KV CacheMulti-Query & Grouped-Query AttentionKV Cache & PagedAttentionPrefix Caching and Prompt CachingFlashAttention & Memory EfficiencyContinuous Batching & SchedulingScaling LLM InferenceModel Parallelism for LLM InferenceModel Quantization: GPTQ, AWQ & GGUFLocal LLM DeploymentSLM Specialization & Edge DeploymentSpeculative DecodingLong Context Window ManagementMixture of Experts ArchitectureMamba & State Space ModelsReasoning & Test-Time ComputeAdvanced MLOps & DevOps for AIGPU Serving & AutoscalingA/B Testing for LLMs
๐Ÿ—๏ธSystem Design Capstones0/9
Content Moderation SystemCode Completion SystemMulti-Tenant LLM PlatformLLM-Powered Search EngineVision-Language Models & CLIPMultimodal LLM ArchitectureDiffusion Models: Images & TextReal-Time Voice AI AgentReasoning Agent System Design
๐ŸŽคAI Lab Interviewing0/4
AI Lab Coding Interview: Python SystemsAI Lab System Design InterviewAI Lab Behavioral InterviewAI Lab Technical Presentation
๐Ÿ”ฌProject Deep Dives0/17
Deep Dive - vLLMDeep Dive - SkyRLDeep Dive - FlashAttentionDeep Dive - FlashInferDeep Dive - DeepGEMMDeep Dive - NCCLDeep Dive - MegatronDeep Dive - DeepSpeedDeep Dive - RayDeep Dive - MLflowDeep Dive - PyTorchDeep Dive - TransformersDeep Dive - SGLangDeep Dive - slimeDeep Dive - DeepEPDeep Dive - TinkerDeep Dive - Light-PEFT
Back to Topics
LearnProduction ML SystemsForecasting and Anomaly Detection
๐Ÿ“ŠMediumEvaluation & Benchmarks

Forecasting and Anomaly Detection

Forecast batch-job demand with time-aware evaluation and turn large forecast errors into reviewable operational alerts.

16 min read
Learning path
Step 46 of 178 in the full curriculum
Ranking and Recommendation SystemsMonitoring Predictive Models

Personalize this lesson

Adapt explanations and teaching visuals to your background and preferred voice.

Friday's cluster ran 160 outbound batch jobs. Last Friday's same-weekday copy said 132. Is tomorrow a staffing problem, or just Friday?

Ranking and recommendation ordered documents for one query at one moment. Capacity planning looks forward in time. Forecasting predicts a later value. Anomaly detection asks whether an observation is unusual enough to inspect. Start with expected volume, then compare the actual count with that expectation. The leftover is the operational signal: unusual enough to inspect, not yet a root cause.

Holdout-week chart of daily batch jobs from Monday through Sunday. A purple weekly-lag forecast stays near a green actual line inside a plus-or-minus three job band until Friday, when actual volume 160 sits far above forecast 132. Aligned residual bars underneath show Friday error plus 28 punching through the same plus-or-minus three band while the other six days stay inside it.
The weekly-lag forecast tracks Monday through Thursday inside a +/-3 band. Friday's observed 160 sits far above forecast 132, so the residual bar of +28 punches through the same band while Saturday and Sunday settle back.

Start with an ordered batch-job series

A time series is a sequence of observations indexed by time. The series here is daily outbound batch-job counts for one model-serving cluster. Each row has a date, a batch-job count, and a flag for a planned model launch. The Friday 160 versus 132 gap is one row in that series.

The forecast horizon is how far ahead you predict. A one-day horizon answers tomorrow's staffing question. A seven-day horizon helps reserve capacity for the coming week. With a weekly seasonal naive baseline, those two horizons use the same numbers: from Sunday's origin, each of the next seven weekdays copies last week's matching day.

Build four weeks of daily counts. The weekday pattern repeats, but small changes keep the series realistic. The final Friday launch event creates a larger jump.

build-batch-job-series.py
1from datetime import date, timedelta 2from hashlib import sha256 3from json import dumps 4from math import ceil, sqrt 5 6start = date(2026, 1, 5) 7weekly_pattern = [100, 112, 115, 118, 132, 82, 76] 8weekly_noise = [ 9 [0, 0, 0, 0, 0, 0, 0], 10 [2, -2, 3, -2, 2, 2, 2], 11 [4, -2, 4, -2, 0, 1, 2], 12 [3, 1, 2, -1, 28, 2, 1], 13] 14 15rows = [] 16for week, noise in enumerate(weekly_noise): 17 for weekday, (baseline, offset) in enumerate(zip(weekly_pattern, noise)): 18 rows.append( 19 { 20 "date": start + timedelta(days=7 * week + weekday), 21 "weekday": weekday, 22 "volume": baseline + offset, 23 "launch_event": week == 3 and weekday == 4, 24 } 25 ) 26 27print("rows:", len(rows)) 28print("first date:", rows[0]["date"]) 29print("last date:", rows[-1]["date"]) 30print("launch_event day:", next(row["date"] for row in rows if row["launch_event"]))
Output
1rows: 28 2first date: 2026-01-05 3last date: 2026-02-01 4launch_event day: 2026-01-30

The fixture stays inspectable. Production evaluation should report separate series or slices by model-serving cluster, service tier, and forecast horizon.

Keep future rows out of training

Ordinary tabular prediction often starts with a random train/test split. That's unsafe for forecasting. A shuffled training set can contain January 31 while its test set contains January 6. A model evaluated that way has learned from the future relative to some test rows.

Compare an invalid shuffled split with a chronological split. The audit prints whether the latest training date reaches or passes the earliest test date.

audit-time-splits.py
1shuffled_train = [0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26] 2shuffled_test = [index for index in range(len(rows)) if index not in shuffled_train] 3time_train = list(range(21)) 4time_test = list(range(21, 28)) 5 6def split_summary(name, train_indices, test_indices): 7 latest_train = max(rows[index]["date"] for index in train_indices) 8 earliest_test = min(rows[index]["date"] for index in test_indices) 9 future_leak = latest_train >= earliest_test 10 print( 11 f"{name}: latest_train={latest_train} " 12 f"earliest_test={earliest_test} future_leak={future_leak}" 13 ) 14 15split_summary("shuffled", shuffled_train, shuffled_test) 16split_summary("time-aware", time_train, time_test) 17assert max(time_train) < min(time_test)
Output
1shuffled: latest_train=2026-01-31 earliest_test=2026-01-06 future_leak=True 2time-aware: latest_train=2026-01-25 earliest_test=2026-01-26 future_leak=False

Hyndman and Athanasopoulos call this time-series cross-validation, or evaluation on a rolling forecasting origin: each test observation occurs after the observations used to make that forecast.[1]Reference 1Forecasting: Principles and Practice, Third Edition.https://otexts.com/fpp3/ You can expand the training history or keep a rolling window. Either way, the time direction stays intact.

Why isn't a random split acceptable just because every row belongs to the same cluster?

Answer

Cluster identity doesn't remove time order. A shuffled split can train on later demand, launch events, and seasonal behavior before evaluating earlier dates. Production only predicts later periods from earlier information.

Establish a same-weekday baseline

Batch-job volume has seasonality when a pattern repeats at a known calendar frequency. Saturday volume stays lower than weekday volume. Copying yesterday into tomorrow treats that weekend lull as the new baseline.

Monday, January 26 actually ran 103 jobs. Sunday was 78. Last Monday was 104.

MethodWhat it copiesMonday forecastActualError
naiveyesterday (Sunday 78)78103+25
seasonal naivelast Monday (104)104103-1

A seasonal naive forecast reuses the observation from the matching previous season. For daily batch-job data with a weekly pattern, forecast Monday from last Monday, Tuesday from last Tuesday, and so on.[1]Reference 1Forecasting: Principles and Practice, Third Edition.https://otexts.com/fpp3/ Hyndman and Athanasopoulos treat this method as a benchmark: a fancier model has to beat it on later windows before the extra machinery is worth running.

Target daySame day last weekForecastActualForecast error
Monday, Jan 26Monday, Jan 19104103-1
Friday, Jan 30Friday, Jan 23132160+28

The Friday arithmetic is 160 - 132 = 28. Positive error means the observed count exceeded the forecast. Negative error means the forecast was too high.

Compute the complete holdout week. Each forecast reads the value exactly seven rows earlier.

forecast-with-weekly-lag.py
1holdout = list(range(21, 28)) 2 3def weekly_lag(index): 4 return rows[index - 7]["volume"] 5 6for index in holdout: 7 forecast = weekly_lag(index) 8 actual = rows[index]["volume"] 9 error = actual - forecast 10 print( 11 f"{rows[index]['date']} forecast={forecast} " 12 f"actual={actual} error={error:+d}" 13 )
Output
12026-01-26 forecast=104 actual=103 error=-1 22026-01-27 forecast=110 actual=113 error=+3 32026-01-28 forecast=119 actual=117 error=-2 42026-01-29 forecast=116 actual=117 error=+1 52026-01-30 forecast=132 actual=160 error=+28 62026-01-31 forecast=83 actual=84 error=+1 72026-02-01 forecast=78 actual=77 error=-1

Saturday shows the same trap in the other direction. A naive one-step forecast would copy Friday's launch (160) into Saturday and miss by -76. The weekly lag copies last Saturday (83) and misses by +1.

Why is last Monday a better forecast for this Monday than yesterday's Sunday count?

Answer

Sunday is a weekend lull. Copying 78 into Monday treats that dip as the new baseline and misses by 25 jobs. Last Monday already lived in the weekday regime, so the error is 1 job.

Measure forecast error in batch jobs

Mean absolute error (MAE) averages the size of each mistake without letting positive and negative errors cancel:[1]Reference 1Forecasting: Principles and Practice, Third Edition.https://otexts.com/fpp3/

MAE=1nโˆ‘t=1nโˆฃytโˆ’y^tโˆฃ\text{MAE} = \frac{1}{n}\sum_{t=1}^{n}|y_t - \hat{y}_t|MAE=n1โ€‹t=1โˆ‘nโ€‹โˆฃytโ€‹โˆ’y^โ€‹tโ€‹โˆฃ

Here nnn is the number of evaluated days, yty_tytโ€‹ is observed volume on day ttt, and y^t\hat{y}_ty^โ€‹tโ€‹ is the forecast for that day. For the holdout week, the absolute weekly-lag errors are 1, 3, 2, 1, 28, 1, 1. Their sum is 37, so MAE is 37 / 7 = 5.3 batch jobs.

MAE stays in batch jobs, which operations can read. It also treats a 28-job miss and a 28-job surplus as equal. Underforecasting a capacity spike can cost more than overforecasting by the same amount, so record asymmetric business cost when the decision requires it.

Calculate overall MAE and normal-day MAE separately. The second metric excludes the known launch-event day so the weekday baseline isn't hidden by one planned spike. Then compare the same week against copying yesterday.

measure-holdout-mae.py
1def mean_absolute_error(errors): 2 return sum(abs(error) for error in errors) / len(errors) 3 4holdout_errors = [ 5 rows[index]["volume"] - weekly_lag(index) 6 for index in holdout 7] 8normal_errors = [ 9 rows[index]["volume"] - weekly_lag(index) 10 for index in holdout 11 if not rows[index]["launch_event"] 12] 13 14naive_errors = [ 15 rows[index]["volume"] - rows[index - 1]["volume"] 16 for index in holdout 17] 18 19print("holdout errors:", holdout_errors) 20print("holdout MAE:", round(mean_absolute_error(holdout_errors), 1)) 21print("normal-day MAE:", round(mean_absolute_error(normal_errors), 1)) 22print("naive holdout MAE:", round(mean_absolute_error(naive_errors), 1))
Output
1holdout errors: [-1, 3, -2, 1, 28, 1, -1] 2holdout MAE: 5.3 3normal-day MAE: 1.5 4naive holdout MAE: 23.6

The weekly lag's holdout MAE is 5.3 batch jobs. Yesterday's lag is 23.6 on the same week. Hyndman and Athanasopoulos recommend this comparison before you spend complexity: if a candidate can't beat last week's same weekday on later windows, don't ship it.[1]Reference 1Forecasting: Principles and Practice, Third Edition.https://otexts.com/fpp3/

Backtest more than one future window

A single future week is fragile evidence. Rolling-origin evaluation moves the forecast origin forward and measures several later windows. In this fixture, the first fold evaluates January 19 through January 25. The second evaluates January 26 through February 1.

Run both folds. The seasonal baseline can forecast each day because each fold starts after at least one full week of history.

run-rolling-origin-backtest.py
1folds = [] 2for origin in (14, 21): 3 indices = list(range(origin, origin + 7)) 4 errors = [ 5 rows[index]["volume"] - weekly_lag(index) 6 for index in indices 7 ] 8 fold = { 9 "start": rows[indices[0]]["date"], 10 "end": rows[indices[-1]]["date"], 11 "mae": round(mean_absolute_error(errors), 1), 12 } 13 folds.append(fold) 14 print(f"{fold['start']} to {fold['end']}: MAE={fold['mae']}")
Output
12026-01-19 to 2026-01-25: MAE=0.9 22026-01-26 to 2026-02-01: MAE=5.3

Backtest by model-serving cluster, service tier, weekday, and launch-event status. A global average can hide one cluster that routinely underforecasts peak demand. The target must also match the decision: submitted job count helps GPU reservation, failed job count helps incident response, and queue-delay count helps operator notification.

Distinguish forecast errors from residuals

Two related quantities often get called residuals. Hyndman and Athanasopoulos separate them on two axes: which rows they use, and how many steps ahead they look.[1]Reference 1Forecasting: Principles and Practice, Third Edition.https://otexts.com/fpp3/

QuantityWhich rowsHow far aheadFormulaWhat it tells you
residualtraining data after fittingusually one-step fitted valuesobserved value minus fitted valuewhether the model captured known history
forecast errorlater data, after the forecast was issuedone-step or multi-stepobserved value minus forecastwhether the model predicted unseen future data

Friday's +28 is a forecast error: Friday's observed volume wasn't available when the forecast was issued. This weekly-lag baseline has no fitted parameters, so a one-step training residual uses the same copy-last-week rule. What still changes is which rows you score: known history versus a later window. An operations team may casually say "residual alert," but the stored artifact should name the measured quantity.

Diagram showing Daily batch-job counts ordered history, Seasonal baseline same weekday, Future forecast expected range, and Observed volume.
Daily batch-job counts ordered history, Seasonal baseline same weekday, Future forecast expected range, and Observed volume.

Calibrate an error band from earlier days

A point forecast such as 132 batch jobs doesn't express uncertainty. A prediction interval pairs a forecast with a lower and upper bound intended to cover a stated proportion of future outcomes. Hyndman and Athanasopoulos treat the interval as part of the forecast, not a chart decoration, and they evaluate it on later data.[1]Reference 1Forecasting: Principles and Practice, Third Edition.https://otexts.com/fpp3/

When one-step errors look roughly Gaussian, their textbook 95% interval is y^tยฑ1.96โ€‰ฯƒ^\hat{y}_{t} \pm 1.96\,\hat{\sigma}y^โ€‹tโ€‹ยฑ1.96ฯƒ^, where ฯƒ^\hat{\sigma}ฯƒ^ is the residual standard deviation. When that shape is doubtful, they fall back to resampling past errors. This lab uses a smaller cousin of that second idea: the 95th percentile of earlier absolute one-day errors, applied as a symmetric band.

Calibrate on January 12 through January 25, before the January 26 holdout starts. Then measure coverage again on the later holdout week. Coverage is the share of days whose absolute error stays inside the band. A band that always covers by growing infinitely wide isn't useful, so keep the width in batch jobs next to the coverage number.

calibrate-empirical-error-band.py
1calibration = list(range(7, 21)) 2calibration_errors = [ 3 rows[index]["volume"] - weekly_lag(index) 4 for index in calibration 5] 6target_coverage = 0.95 7interval_policy = "empirical-absolute-error-p95-v1" 8 9def nearest_rank_quantile(values, probability): 10 ordered = sorted(values) 11 rank = max(0, ceil(probability * len(ordered)) - 1) 12 return ordered[rank] 13 14error_band = nearest_rank_quantile( 15 [abs(error) for error in calibration_errors], 16 probability=target_coverage, 17) 18calibration_coverage = sum( 19 abs(error) <= error_band 20 for error in calibration_errors 21) / len(calibration_errors) 22holdout_coverage = sum( 23 abs(error) <= error_band 24 for error in holdout_errors 25) / len(holdout_errors) 26 27residual_sd = sqrt(sum(error * error for error in calibration_errors) / len(calibration_errors)) 28normal_half_width = round(1.96 * residual_sd, 1) 29 30assert max(calibration) < min(holdout) 31print("calibration errors:", calibration_errors) 32print("95% empirical absolute-error band:", error_band) 33print("residual sd:", round(residual_sd, 2)) 34print("normal 95% half-width:", normal_half_width) 35print("in-sample calibration coverage:", round(calibration_coverage, 3)) 36print("later holdout coverage:", round(holdout_coverage, 3))
Output
1calibration errors: [2, -2, 3, -2, 2, 2, 2, 2, 0, 1, 0, -2, -1, 0] 295% empirical absolute-error band: 3 3residual sd: 1.75 4normal 95% half-width: 3.4 5in-sample calibration coverage: 1.0 6later holdout coverage: 0.857

The empirical band is +/-3 batch jobs. The normal-theory cousin is about +/-3.4. Both widths agree that Friday's +28 isn't ordinary noise. Neither width is a production 95% guarantee from fourteen calibration errors. In-sample coverage is 1.000 because the band was chosen on those same errors. Later holdout coverage is 6 / 7 = 0.857 because Friday falls outside. A production system should measure out-of-sample coverage across rolling windows, forecast horizons, model-serving clusters, and important demand slices, and it should keep the interval narrow enough to act on.

Why doesn't the tiny +/-3 band prove that future batch-job counts will land inside it 95% of the time?

Answer

It comes from only fourteen earlier errors. In-sample calibration coverage tells you how the band was chosen; later holdout coverage checks whether it transferred to unseen days. One holdout week still isn't enough. A production interval needs enough history plus out-of-sample coverage checks across rolling windows, forecast horizons, and important slices.

Turn an unusual error into a review artifact

An anomaly is an observation unusual enough to inspect. It isn't proof that a cluster, pipeline, or model failed. Friday's jump could come from a planned model launch, duplicated event ingestion, a large backfill, or genuine demand growth.

Apply the empirical band to the held-out week. The alert stores enough context to reproduce the comparison.

create-capacity-alert.py
1forecast_version = "weekly-lag-v1" 2training_cutoff = str(rows[20]["date"]) 3owner = "capacity-ops" 4 5alerts = [] 6for index in holdout: 7 forecast = weekly_lag(index) 8 actual = rows[index]["volume"] 9 forecast_error = actual - forecast 10 if abs(forecast_error) > error_band: 11 alerts.append( 12 { 13 "series": "fc-a.batch_jobs", 14 "forecast_version": forecast_version, 15 "training_cutoff": training_cutoff, 16 "interval_policy": interval_policy, 17 "date": str(rows[index]["date"]), 18 "forecast": forecast, 19 "actual": actual, 20 "forecast_error": forecast_error, 21 "lower": forecast - error_band, 22 "upper": forecast + error_band, 23 "launch_event": rows[index]["launch_event"], 24 "action": "review", 25 "owner": owner, 26 "resolution": "pending", 27 } 28 ) 29 30for alert in alerts: 31 print(alert)
Output
1{'series': 'fc-a.batch_jobs', 'forecast_version': 'weekly-lag-v1', 'training_cutoff': '2026-01-25', 'interval_policy': 'empirical-absolute-error-p95-v1', 'date': '2026-01-30', 'forecast': 132, 'actual': 160, 'forecast_error': 28, 'lower': 129, 'upper': 135, 'launch_event': True, 'action': 'review', 'owner': 'capacity-ops', 'resolution': 'pending'}

The baseline caught Friday because 160 falls outside [129, 135]. The launch-event flag doesn't erase the event. It changes the first investigation step.

Route alerts with context. A planned model launch still deserves capacity review, but it shouldn't automatically create an incident page.

route-alert-with-context.py
1def route_alert(alert): 2 if alert["launch_event"]: 3 return "review planned model launch capacity" 4 return "page unexpected batch-job spike" 5 6for alert in alerts: 7 print(alert["date"], "->", route_alert(alert))
Output
12026-01-30 -> review planned model launch capacity

Log the forecast version, training cutoff, interval policy version, observed value, error, known-event flags, owner, and eventual resolution. That receipt helps operations act now and helps future model reviews learn from the alert.

Evaluate alert usefulness separately

Low MAE doesn't guarantee useful alerts. An alert threshold can still page too often or miss disruptions. Once reviewers label whether each historical alert was actionable, measure alert precision and recall:

MetricQuestion
alert precisionOf the alerts sent to review, how many were actionable?
alert recallOf the actionable disruptions, how many did the policy surface?

Compute both metrics on a five-event resolution fixture. The policy surfaced two useful events, sent one noisy alert, and missed one disruption.

evaluate-alert-policy.py
1resolved_events = [ 2 {"alert": True, "actionable": True}, 3 {"alert": True, "actionable": False}, 4 {"alert": False, "actionable": True}, 5 {"alert": False, "actionable": False}, 6 {"alert": True, "actionable": True}, 7] 8 9true_positives = sum(event["alert"] and event["actionable"] for event in resolved_events) 10false_positives = sum(event["alert"] and not event["actionable"] for event in resolved_events) 11false_negatives = sum(not event["alert"] and event["actionable"] for event in resolved_events) 12 13def rate_or_none(numerator, denominator): 14 return round(numerator / denominator, 3) if denominator else None 15 16alert_precision = rate_or_none(true_positives, true_positives + false_positives) 17alert_recall = rate_or_none(true_positives, true_positives + false_negatives) 18print("alert precision:", alert_precision) 19print("alert recall:", alert_recall)
Output
1alert precision: 0.667 2alert recall: 0.667

Tune the policy against operational cost. A missed cluster disruption and a noisy review ticket don't have equal consequences. If a denominator is zero, report None: no evidence isn't the same as measured failure. Page only when urgency warrants interruption; otherwise queue a review with the same evidence.

Reproduce a data-quality anomaly

Not every spike represents demand. A duplicated telemetry event can inflate a count before forecasting code sees it. Reproduce that failure with three raw telemetry rows, two of which describe the same batch job event.

detect-duplicate-telemetry-events.py
1raw_job_events = [ 2 {"event_id": "job-001", "job_id": "batch-100", "type": "started"}, 3 {"event_id": "job-001", "job_id": "batch-100", "type": "started"}, 4 {"event_id": "job-002", "job_id": "batch-101", "type": "started"}, 5] 6unique_events = { 7 event["event_id"]: event 8 for event in raw_job_events 9} 10deduplication_policy = "telemetry-event-id-v1" 11duplicate_rows = len(raw_job_events) - len(unique_events) 12 13print("naive started-job count:", len(raw_job_events)) 14print("deduplicated started-job count:", len(unique_events)) 15print("duplicate rows:", duplicate_rows)
Output
1naive started-job count: 3 2deduplicated started-job count: 2 3duplicate rows: 1

An anomaly review should inspect input quality before retraining a model. Retraining on duplicated counts would teach the model to copy a pipeline bug. This tiny dictionary deduper keeps one row because the repeated payloads match. If two rows reuse an event ID but disagree, quarantine the conflict instead of silently choosing one.

Why shouldn't the alert policy declare an incident as soon as a count crosses the error band?

Answer

The forecast error proves that the observation is unusual relative to the baseline. It doesn't identify the cause. Known launch events, duplicated ingestion, and genuine demand shifts require different responses.

Gate and publish a forecast candidate

A minimal release gate should prove time order, calibration order, normal-day accuracy, interval bookkeeping, contextualized alerts, and assigned ownership. These checks don't prove the baseline is production-ready, but they prevent avoidable mistakes from reaching a capacity workflow.

Run the gate.

check-forecast-release-gates.py
1gates = { 2 "time_order": max(time_train) < min(time_test), 3 "calibration_before_holdout": max(calibration) < min(holdout), 4 "normal_day_mae": mean_absolute_error(normal_errors) <= 3.0, 5 "calibration_coverage_recorded": 0.0 <= calibration_coverage <= 1.0, 6 "holdout_coverage_recorded": 0.0 <= holdout_coverage <= 1.0, 7 "interval_policy_versioned": bool(interval_policy), 8 "deduplication_policy_versioned": bool(deduplication_policy), 9 "alerts_have_context": all( 10 { 11 "series", "forecast_version", "training_cutoff", "interval_policy", 12 "date", "forecast", "actual", "forecast_error", "lower", "upper", 13 "launch_event", "action", "owner", "resolution", 14 } <= alert.keys() 15 for alert in alerts 16 ), 17 "owner_assigned": bool(owner), 18} 19 20for name, passed in gates.items(): 21 print(f"{name}: {passed}") 22print("release gate:", all(gates.values()))
Output
1time_order: True 2calibration_before_holdout: True 3normal_day_mae: True 4calibration_coverage_recorded: True 5holdout_coverage_recorded: True 6interval_policy_versioned: True 7deduplication_policy_versioned: True 8alerts_have_context: True 9owner_assigned: True 10release gate: True

Publish a receipt. The hash lets a later monitoring job identify the exact baseline, interval, and input policies it evaluates.

publish-forecast-candidate-receipt.py
1receipt = { 2 "series": "fc-a.batch_jobs", 3 "forecast": forecast_version, 4 "training_cutoff": training_cutoff, 5 "interval_policy": { 6 "version": interval_policy, 7 "target_coverage": target_coverage, 8 "absolute_error_band": error_band, 9 "calibration_window": [str(rows[7]["date"]), str(rows[20]["date"])], 10 "calibration_coverage": round(calibration_coverage, 3), 11 "holdout_window": [str(rows[21]["date"]), str(rows[27]["date"])], 12 "holdout_coverage": round(holdout_coverage, 3), 13 }, 14 "input_policy": { 15 "deduplication": deduplication_policy, 16 }, 17 "evaluation": { 18 "normal_day_mae": round(mean_absolute_error(normal_errors), 1), 19 "alert_count": len(alerts), 20 }, 21 "alerts": alerts, 22 "release_checks": gates, 23 "owner": owner, 24 "status": "candidate_for_shadow" if all(gates.values()) else "blocked", 25} 26payload = dumps(receipt, sort_keys=True, separators=(",", ":")) 27 28print("status:", receipt["status"]) 29print("evaluation:", receipt["evaluation"]) 30print("holdout coverage:", receipt["interval_policy"]["holdout_coverage"]) 31print("receipt sha256:", sha256(payload.encode()).hexdigest()[:12])
Output
1status: candidate_for_shadow 2evaluation: {'normal_day_mae': 1.5, 'alert_count': 1} 3holdout coverage: 0.857 4receipt sha256: a40b84aed22b

The receipt keeps calibration evidence separate from later holdout evidence and binds the policies that produced both. Passing these checks earns shadow evaluation, not a production rollout. The hash is what a later monitoring job should look up when it asks whether live inputs, delayed labels, or a newer candidate should replace this baseline.

When the policy breaks

SymptomCauseFix
Test results collapse at launchrandom split leaked future behavioruse chronological rolling-origin validation
Alerts fire every Mondayyesterday's Sunday lull became Monday's forecaststart with a same-weekday baseline
Interval looks precise but misses peaksband was calibrated in sample or on too little historymeasure out-of-sample coverage by horizon and slice
Promotion creates an incident pagealert threshold has no business contextroute known events to capacity review
Model learns a fake demand jumpduplicated events entered training rowsdeduplicate and audit ingestion before retraining
Operations ignores alertsreceipt lacks ownership or resolutionlog policy version, context, owner, and outcome
Complete the lesson

Mastery Check

Answer every question, then check your score. Score 75% or higher to mark this lesson complete.

1.A forecast split for one cluster has latest_train=2026-01-31 and earliest_test=2026-01-06. What does this imply?

Correct answer: The split leaks future information because training includes dates after test dates.

Forecast evaluation must mimic production: each evaluated date should occur after the data used to train the forecast. Same-cluster identity doesn't remove time order. A chronological or rolling-origin split preserves that invariant; a shuffled split can learn later demand or launch events before testing earlier dates.

2.Monday actual volume is 103. Sunday was 78 and last Monday was 104. What do the naive and seasonal naive one-step forecasts record?

Correct answer: Naive forecast 78 with error +25; seasonal naive forecast 104 with error -1.

Naive copies yesterday, so Monday copies Sunday's 78 and misses by 103 - 78 = +25. Seasonal naive copies last Monday's 104 and misses by 103 - 104 = -1. Forecast error is actual minus forecast.

3.A holdout week's forecast errors are [-1, 3, -2, 1, 28, 1, -1], and the +28 launch_event day may have a higher underforecasting cost than an equal overforecast. Which report is valid?

Correct answer: Overall MAE is 5.3 batch jobs and normal-day MAE is 1.5; also report asymmetric capacity cost.

MAE uses absolute errors, so the total is 37 and overall MAE is 37 / 7 = 5.3 batch jobs. Excluding the known launch event gives 9 / 6 = 1.5 batch jobs for normal days. Keep both views, and add an asymmetric cost measure when a positive capacity error is more expensive than an equal negative error because MAE weights their magnitudes equally.

4.A team adds a two-day forecast horizon for daily batch-job counts and wants to backtest it. Which evaluation setup preserves the required time order?

Correct answer: For each target t, use only information available no later than t - 2, and score several later windows as the origin moves forward.

A two-day horizon means the prediction for time t must be made from information available no later than t - 2. Rolling-origin evaluation then moves the forecast origin forward to test more than one future window. Shuffling, using t - 1 information, or calibrating on the holdout breaks the production time order.

5.A forecast for Jan 30 was issued before Jan 30 using a same-weekday value of 132. The observed count later became 160. How should the +28 be recorded?

Correct answer: As a forecast error, because it's actual minus a forecast made before the outcome was known.

A residual comes from training data after fitting a model and compares an observed value with a fitted value. This Jan 30 value wasn't available when the forecast was issued, so 160 - 132 = +28 is a forecast error on unseen future data.

6.An empirical interval policy uses 14 earlier absolute one-day errors and selects a +/-3 batch job band for target coverage 0.95. Calibration coverage is 1.0, but later holdout coverage is 6/7. Which conclusion is valid?

Correct answer: The band is a small calibration result that still needs out-of-sample coverage checks across more rolling windows and slices.

The band was chosen from only 14 earlier errors, so its in-sample calibration coverage mostly describes the fitting procedure. Later coverage on unseen windows is the evidence that matters, and one holdout week is still too little for a production coverage claim.

7.A Friday count of 160 is compared with a forecast of 132 and a +/-3 batch job band. The row has launch_event=True. What should the alert policy do?

Correct answer: Create a review artifact and route it to planned launch-capacity review.

The absolute forecast error is 28, which is larger than the 3 batch job band, so the event is unusual enough to review. The launch-event flag changes the first investigation path; it doesn't erase the alert or prove an incident. A large forecast error isn't a training residual or a root-cause diagnosis.

8.Five reviewed events have (alert, actionable) values: (True, True), (True, False), (False, True), (False, False), (True, True). What are the alert precision and alert recall?

Correct answer: Precision 0.667 and recall 0.667.

There are 2 true positives, 1 false positive, and 1 false negative. Precision asks how many sent alerts were actionable: 2/(2+1) = 0.667. Recall asks how many actionable disruptions were surfaced: 2/(2+1) = 0.667.

9.An anomaly review finds three raw telemetry rows, but two rows share event_id job-001 and the same batch job payload. What is the safest next step before retraining?

Correct answer: Deduplicate by event_id and inspect telemetry ingestion before retraining.

A duplicated telemetry event can inflate the count before forecasting code sees it. Matching duplicate event IDs should be deduplicated under a recorded policy, and conflicting reused IDs should be quarantined rather than silently chosen. Retraining first risks teaching the model to reproduce a pipeline bug.

10.A forecast candidate has acceptable normal-day MAE, but its interval policy version is blank and alert records omit owner and resolution fields. What should the release gate do?

Correct answer: Block the candidate until the interval policy is versioned and each alert has reproducible context and ownership.

The gate checks more than accuracy. It also verifies time order, calibration order, versioned interval and deduplication policies, alert context, owner assignment, and recorded coverage. If required policy or alert fields are missing, the candidate should be blocked rather than silently published.

10 questions remaining.

Next Step
Continue to Monitoring Predictive Models

A versioned forecast candidate turns unusual future errors into review artifacts. Monitoring puts that candidate on a live clock: <span data-glossary="feature-freshness">feature freshness</span>, delayed labels, and whether a newer model should replace or <span data-glossary="rollback">roll back</span> the current release.

PreviousRanking and Recommendation Systems
Share this article
XFacebookLinkedInBlueskyRedditHacker NewsEmail
References

Forecasting: Principles and Practice, Third Edition.

Hyndman, R. J. & Athanasopoulos, G. ยท 2021

https://otexts.com/fpp3/

Discussion

Questions and insights from fellow learners.

Discussion loads when you reach this section.