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 177 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/16
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 TuningReward 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 & Test-Time Compute
๐ŸŽค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.

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

Personalize this lesson

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

Ranking and recommendation chose an ordered slate at one moment. Forecasting asks the next production question: how many batch jobs will run tomorrow, and which observed counts fall outside that expectation?

Forecasting predicts a value for a later time. Anomaly detection asks whether an observation is unusual enough to inspect. The two jobs fit together: first estimate expected batch-job volume, then compare the actual count with that expectation. Monitoring chapters later reuse the same residual signal as a live operational check.

Seven-day batch job forecast chart comparing a weekly-lag baseline and its plus-or-minus three batch job band with observed volumes, highlighting Friday actual volume 160 against forecast 132 as a plus-28 review event, alongside holdout error and interval coverage evidence.
The weekly-lag baseline keeps normal-day MAE at 1.5 batch jobs. Friday's observed 160 falls well outside the 129..135 interval around forecast 132, creating a +28 launch review rather than an automatic incident.

Start with an ordered batch-job series

A time series is a sequence of observations indexed by time. This lesson uses 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 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. This lab predicts one day at a time, then evaluates a full future week.

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 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 describe time-series cross-validation as repeated evaluation where each test observation occurs after the observations used for training.[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 may stay lower than weekday volume. Predicting tomorrow from yesterday alone will overreact around each weekend.

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/

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

Simple baselines matter. A complex model should beat the seasonal naive forecast on future windows before its extra features and operational cost are justified.

Measure forecast error in batch jobs

Mean absolute error (MAE) averages the size of each mistake without letting positive and negative errors cancel:

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 errors are 1, 3, 2, 1, 28, 1, 1. Their sum is 37, so MAE is 37 / 7 = 5.3 batch jobs.

Calculate overall MAE and normal-day MAE separately. The second metric excludes the known launch event day so you can see baseline behavior without hiding the operational spike.

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 14print("holdout errors:", holdout_errors) 15print("holdout MAE:", round(mean_absolute_error(holdout_errors), 1)) 16print("normal-day MAE:", round(mean_absolute_error(normal_errors), 1))
Output
1holdout errors: [-1, 3, -2, 1, 28, 1, -1] 2holdout MAE: 5.3 3normal-day MAE: 1.5

MAE remains in batch jobs, which makes it understandable to operations. It doesn't tell you everything: underforecasting a capacity spike may cost more than overforecasting by the same amount. Record asymmetric business cost when the decision requires it.

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, but the distinction is useful:

QuantityWhere it comes fromFormulaWhat it tells you
residualtraining data after fitting a modelobserved value minus fitted valuewhether model fits known history
forecast errorlater observation after making a forecastobserved value minus forecastwhether model predicted unseen future data

Hyndman and Athanasopoulos make this distinction explicitly.[1]Reference 1Forecasting: Principles and Practice, Third Edition.https://otexts.com/fpp3/ Our Friday +28 is a forecast error because Friday's observed volume wasn't available when the forecast was issued. An operations team may casually say "residual alert," but the stored artifact should name the measured quantity precisely.

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. Forecast intervals should be evaluated for coverage, not displayed beside the point estimate alone.[1]Reference 1Forecasting: Principles and Practice, Third Edition.https://otexts.com/fpp3/

For a small first-principles exercise, use the 95th percentile of earlier absolute one-day forecast errors. This produces a symmetric empirical error band. The code deliberately calibrates on January 12 through January 25, before the January 26 holdout starts, then measures coverage again on the later holdout week.

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 27assert max(calibration) < min(holdout) 28print("calibration errors:", calibration_errors) 29print("95% empirical absolute-error band:", error_band) 30print("in-sample calibration coverage:", round(calibration_coverage, 3)) 31print("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 3in-sample calibration coverage: 1.0 4later holdout coverage: 0.857

The ยฑ3 band explains the mechanics, not a production guarantee. Fourteen calibration errors are too few for a stable 95% interval. Its in-sample calibration coverage is 1.000, while later holdout coverage is 6 / 7 = 0.857 because Friday falls outside the band. A production system should measure out-of-sample interval coverage across rolling windows, forecast horizons, model-serving clusters, and important demand slices.

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 gates earns shadow evaluation, not a production rollout.

When the policy breaks

SymptomCauseFix
Test results collapse at launchrandom split leaked future behavioruse chronological rolling-origin validation
Alerts fire every Mondayweekly seasonality is absentadd a same-weekday baseline first
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 does not 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.For a daily batch job series with weekly seasonality, the target is Tuesday Jan 27. Last Tuesday's volume was 110 and the actual target count is 113. Under a seasonal naive weekly-lag baseline, what forecast error is recorded?

Correct answer: Forecast 110 and forecast_error +3.

A seasonal naive weekly-lag forecast reuses the observation from the matching weekday one week earlier. Forecast error is actual minus forecast, so the recorded error is 113 - 110 = +3 batch jobs.

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 is 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 was not 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 model launch capacity review, not an automatic incident page.

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 does not erase the alert or prove an incident. A large forecast error is not the same thing as 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

You can issue a time-aware forecast candidate and turn unusual future errors into reviewable alerts. Next, monitor deployed models for <span data-glossary="data-drift">input shift</span>, delayed outcome degradation, and retraining decisions.

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.