Tuning Thresholds for Scheduling Accuracy
Every automated traffic pipeline eventually lives or dies on a handful of numbers: how many seconds of drift is tolerable before a placement is flagged, how tight a gap between two spots is legal, how saturated a daypart may become before the solver stops accepting inventory. Set those tolerances too loose and the engine commits over-delivered breaks, competitive-adjacency breaches, and unresolvable make-goods that surface only during as-run reconciliation. Set them too tight and the same engine rejects perfectly valid placements the moment a network feed runs thirty seconds long, halting rotation and dumping work onto a manual traffic desk. This guide covers how to model, implement, and calibrate those tolerance parameters as first-class controls inside the Spot Scheduling Validation & Rule Engines architecture. It is written for traffic managers who need to understand why a threshold fired, and for Python automation engineers who need a deployable evaluator with strict typing, structured logging, and an audit trail that survives an FCC inspection.
Concept & Data Model
A threshold is not a constant; it is a control surface that the scheduler consults on every candidate placement. The engine reads a normalized subset of the canonical spot schema — spot_id, duration_frames, daypart, separation_seconds, billing_code — and evaluates it against a threshold profile: a versioned set of tolerance parameters keyed by daypart and station. The critical modeling decision is the separation of two parameter classes that behave very differently under pressure:
- Soft tolerances absorb predictable, benign variance — encoder lead time, sub-frame rounding, a program boundary that shifts by a few seconds. These may expand and contract with operational conditions.
- Hard floors encode contractual and regulatory minimums — the FCC competitive-separation minimum, an advertiser frequency cap, a political-inventory disclosure requirement. These never relax, regardless of how much drift the engine is trying to absorb.
Conflating the two is the single most common design fault in threshold systems: an operator widens a tolerance to stop false conflict flags during a sports overrun and silently opens a hole below the regulatory separation minimum. The data model must make that impossible by construction, which is why the profile below stores floors and tolerances as distinct, independently-clamped fields.
The table below is the parameter set the evaluator actually reads. Every field is typed and range-constrained before a profile is loaded; a malformed profile is rejected at ingestion, never silently coerced.
| Parameter | Type | Constraint | Unit | Role in validation |
|---|---|---|---|---|
max_drift_seconds |
float |
> 0, <= 120 |
seconds | Rejects placements whose scheduled time diverges from the reference log beyond tolerance |
min_gap_seconds |
float |
>= 0 |
seconds | Enforces inter-spot spacing; guards against overlapping avails |
saturation_cap_pct |
float |
0 < x <= 1.0 |
ratio | Caps filled inventory per daypart to prevent over-delivery |
separation_floor_seconds |
int |
>= contract minimum |
seconds | Hard floor: competitive/political adjacency minimum, never widened |
encoder_lead_frames |
int |
>= 0 |
frames | Compensates known playout-server latency before drift is measured |
max_consecutive_spots |
int |
1–9 |
count | Frequency guard within a rotation window |
profile_version |
str |
semver, non-null | — | Audit key; every commit records the profile that judged it |
A candidate placement passes through a sequential gate: drift is checked first (it is the cheapest and most common failure), then gap, then saturation. Only a placement that clears every gate is accepted; any breach short-circuits to a review or make-good route rather than mutating partial schedule state.
Figure — A candidate placement traverses a sequential threshold gate (drift, gap, saturation cap); only placements clearing every check are accepted, otherwise they are rejected and routed to review.
Implementation Approach
The core design question is how a threshold acquires its value. Three strategies sit on a spectrum of sophistication and operational risk.
Static constants are the simplest: a single ±10s drift tolerance compiled into the scheduler. They are trivially auditable and deterministic, but they degrade badly under real traffic — the same tolerance that is correct at 3 a.m. is far too tight during a live sports overrun and too loose during a quiet overnight block. Static constants are the right starting point and the wrong steady state.
Per-daypart profiles are the pragmatic default. Each daypart carries its own profile, so prime time can enforce tighter separation and lower fallback tolerance than daytime without a code change. This is a lookup-table approach: profiles are configuration, not code, and can be revised by a traffic manager without redeploying the solver. The trade-off is that profiles are still fixed within a daypart and cannot react to a boundary that shifts during the block.
Adaptive calibration closes that gap by treating tolerance as a stateful variable driven by live telemetry — a proportional response to observed drift, clamped to operational bounds and then intersected against the hard floor. This is powerful and dangerous in equal measure: an unbounded feedback loop will chase noise into runaway expansion. The full bounded-adjustment algorithm, its quarantine logic, and its audit payload are documented in the companion how-to, Adjusting Thresholds for Automated Scheduling. The design rule that keeps adaptation safe is simple: adaptation may only ever widen a soft tolerance, and only the hard floor may veto a commit.
Whichever strategy you choose, threshold evaluation must run before conflict resolution, not after. Overlapping placements have to be spaced correctly before the engine tries to arbitrate priority, so the gate here feeds directly into Detecting Time Slot Conflicts in Traffic Logs, which isolates exact collision boundaries once spacing is validated. Thresholds also interlock with Building Rule Engines for Spot Rotation: the max_consecutive_spots and separation parameters are the same values the rotation engine reads to enforce frequency caps, so both subsystems must load from one profile source of truth rather than duplicating constants.
Production Python Implementation
The reference implementation is a typed evaluator plus a circuit breaker. The evaluator is pure over its inputs — the same candidate and profile always produce the same result — which is the property that makes an audit replay byte-identical. The ThresholdProfile is a Pydantic model so a malformed profile is rejected at load time, not mid-schedule. Every decision emits a structured log line in the traffic-ops format timestamp | level | module | message, carrying the spot_id so an auditor can reconstruct the ruling from logs alone.
from __future__ import annotations
import logging
from datetime import datetime, timezone
from enum import Enum
from pydantic import BaseModel, Field, model_validator
# Traffic-ops structured logging: timestamp | level | module | message
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s | %(levelname)s | %(name)s | %(message)s",
)
logger = logging.getLogger("threshold.evaluate")
class Violation(str, Enum):
DRIFT_EXCEEDED = "drift_exceeded"
GAP_VIOLATION = "gap_violation"
SATURATION_EXCEEDED = "saturation_exceeded"
SEPARATION_UNDERFLOW = "separation_underflow" # hard-floor breach
class ThresholdProfile(BaseModel):
"""Versioned tolerance profile, keyed by daypart at load time.
Soft tolerances may be widened by calibration; the separation floor
is a hard regulatory minimum and is never relaxed."""
profile_version: str = Field(..., min_length=1)
daypart: str
max_drift_seconds: float = Field(..., gt=0, le=120)
min_gap_seconds: float = Field(..., ge=0)
saturation_cap_pct: float = Field(..., gt=0, le=1.0)
separation_floor_seconds: int = Field(..., ge=0) # HARD floor
encoder_lead_frames: int = Field(..., ge=0)
frame_rate: float = Field(29.97, gt=0)
@model_validator(mode="after")
def _floor_not_below_contract(self) -> "ThresholdProfile":
# A soft tolerance can never be authored below the hard floor it
# is supposed to protect — catch the misconfiguration at load.
if self.min_gap_seconds > self.separation_floor_seconds:
logger.warning(
"profile=%s daypart=%s gap %.1fs exceeds separation floor %ds",
self.profile_version, self.daypart,
self.min_gap_seconds, self.separation_floor_seconds,
)
return self
class Candidate(BaseModel):
spot_id: str = Field(..., min_length=1)
scheduled_time: datetime # UTC, timezone-aware
reference_time: datetime # air time from the reference log (UTC)
duration_seconds: float = Field(..., gt=0) # zero-duration avails are illegal
prev_end_time: datetime | None = None # end of the preceding spot, if any
separation_seconds: int = Field(..., ge=0) # gap to nearest same-advertiser spot
daypart_fill_pct: float = Field(..., ge=0) # projected fill AFTER this placement
class Ruling(BaseModel):
spot_id: str
accepted: bool
violations: list[Violation] = Field(default_factory=list)
profile_version: str
evaluated_at: str
class ThresholdEvaluator:
"""Deterministic, idempotent threshold gate. Evaluates one candidate
against a loaded profile and returns a signed ruling. Hard-floor
breaches are non-negotiable regardless of soft-tolerance state."""
def __init__(self, profile: ThresholdProfile) -> None:
self.profile = profile
self._lead_seconds = profile.encoder_lead_frames / profile.frame_rate
def evaluate(self, candidate: Candidate) -> Ruling:
p = self.profile
violations: list[Violation] = []
# 1. Drift: divergence from the reference air time, net of the known
# encoder lead. Compare on absolute value so early/late are equal.
drift = abs(
(candidate.scheduled_time - candidate.reference_time).total_seconds()
) - self._lead_seconds
if drift > p.max_drift_seconds:
violations.append(Violation.DRIFT_EXCEEDED)
# 2. Gap: spacing to the preceding spot must clear the soft minimum.
if candidate.prev_end_time is not None:
gap = (candidate.scheduled_time - candidate.prev_end_time).total_seconds()
if gap < p.min_gap_seconds:
violations.append(Violation.GAP_VIOLATION)
# 3. Saturation: projected daypart fill may not exceed the cap.
if candidate.daypart_fill_pct > p.saturation_cap_pct:
violations.append(Violation.SATURATION_EXCEEDED)
# 4. HARD FLOOR: competitive/political separation. This is checked
# last and outranks everything — no soft tolerance can waive it.
if candidate.separation_seconds < p.separation_floor_seconds:
violations.append(Violation.SEPARATION_UNDERFLOW)
accepted = not violations
level = logging.INFO if accepted else logging.WARNING
logger.log(
level,
"spot_id=%s profile=%s accepted=%s violations=%s drift=%.2fs",
candidate.spot_id, p.profile_version, accepted,
",".join(v.value for v in violations) or "none", drift,
)
return Ruling(
spot_id=candidate.spot_id,
accepted=accepted,
violations=violations,
profile_version=p.profile_version,
evaluated_at=datetime.now(timezone.utc).isoformat(),
)
A representative accepted-path log line reads 2026-07-03T14:22:07+00:00 | INFO | threshold.evaluate | spot_id=6ba7… profile=prime-2.3.0 accepted=True violations=none drift=1.80s. Because evaluate is pure over (candidate, profile), replaying the same order during an audit reproduces the ruling exactly.
Thresholds are only safe if the pipeline stops committing when a dependency the evaluator relies on — a drift-telemetry feed, a separation lookup — starts failing. That guardrail is a circuit breaker, and the parent scheduling architecture points here for its concrete state machine:
import time
from enum import Enum
class BreakerState(str, Enum):
CLOSED = "closed" # healthy — traffic flows
OPEN = "open" # failing — commits halted
HALF_OPEN = "half_open" # probing recovery
class CircuitBreaker:
"""Halts schedule commits when a threshold dependency degrades, rather
than letting the engine emit rulings from stale telemetry."""
def __init__(self, fail_max: int = 5, reset_timeout: float = 30.0) -> None:
self.fail_max = fail_max
self.reset_timeout = reset_timeout
self._failures = 0
self._opened_at = 0.0
self.state = BreakerState.CLOSED
def allow(self) -> bool:
if self.state is BreakerState.OPEN:
if time.monotonic() - self._opened_at >= self.reset_timeout:
self.state = BreakerState.HALF_OPEN
logger.info("spot_id=- breaker=half_open probing recovery")
return True
return False
return True
def record_success(self) -> None:
self._failures = 0
if self.state is not BreakerState.CLOSED:
self.state = BreakerState.CLOSED
logger.info("spot_id=- breaker=closed recovered")
def record_failure(self) -> None:
self._failures += 1
if self._failures >= self.fail_max:
self.state = BreakerState.OPEN
self._opened_at = time.monotonic()
logger.warning(
"spot_id=- breaker=open failures=%d halting commits",
self._failures,
)
When the breaker is open, the scheduler holds inventory in a durable queue instead of guessing; on recovery it drains, and because every ruling is idempotent on spot_id, the redelivered backlog never double-books.
Validation & Edge Cases
Broadcast operations generate boundary conditions that a naive tolerance check gets wrong. Each of the following must be handled explicitly.
- Timezone offsets and DST. All comparisons happen in UTC. A drift check that subtracts a naive local timestamp will report a full-hour phantom drift twice a year at the DST boundary. The evaluator only accepts timezone-aware
datetimevalues; the ingestion layer normalizes to UTC before a record is ever seen here, exactly as Avails Mapping Strategies for Linear TV requires for clearance windows. - Sports overruns. A live event that runs long pushes every subsequent program boundary. A fixed drift tolerance will flag an entire hour of downstream spots as conflicts. The correct response is a temporary, bounded widening of the soft drift tolerance only — never the separation floor — driven by the adaptive path in the companion how-to.
- Preemption tiers. Not every drift is equal. A placement displaced by an EAS activation is a hard preemption that outranks contractual guarantees and routes straight to make-good handling; a placement drifting because of encoder latency is a soft event the tolerance should absorb. The evaluator must know which tier it is judging.
- Competitive separation. The
separation_floor_secondsguard is the one check that must fire even when every soft tolerance is maximally relaxed. Two spots from competing advertisers landing inside the floor is a contractual breach, not a warning, and the ruling reflects that with a dedicatedSEPARATION_UNDERFLOWviolation. - Zero-duration avails. A
duration_secondsof zero is illegal and is rejected upstream by the Pydanticgt=0constraint; it must never reach the saturation math, where it would divide fill percentages toward nonsense. - Sub-frame rounding. Encoder lead time is expressed in frames and converted with the profile’s
frame_rate(29.97 for NTSC). Rounding frames to whole seconds before the subtraction introduces up to a half-second of phantom drift per placement — enough to trip a tight prime-time tolerance — so the conversion stays in floating-point seconds.
Integration Points
This subsystem sits between ingestion and the scheduling commit. Upstream, it consumes records already normalized and typed by the pipeline described in Schema Validation with Pydantic for Traffic Data — it never parses vendor formats itself. Downstream, an accepted Ruling gates the commit, and every ruling (accepted or rejected) is emitted as a telemetry event so calibration and billing reconciliation can both consume it.
The event contract is a versioned JSON message, published to the same durable broker the rest of the engine uses so delivery is at-least-once and replayable:
{
"event": "threshold.ruling",
"schema_version": "1.1.0",
"spot_id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"profile_version": "prime-2.3.0",
"accepted": false,
"violations": ["drift_exceeded"],
"drift_seconds": 14.6,
"daypart": "prime",
"evaluated_at": "2026-07-03T14:22:07Z",
"billing_code": "POL2026A"
}
The billing_code travels with the event so the reconciliation join key survives into billing without a second lookup, in the normalized form defined by Standardizing Billing Codes Across Traffic Systems. Ruling events are also the raw material for observability: violation rates and drift distributions per daypart are the signal that tells a traffic manager a profile has drifted out of calibration and needs revision. The evaluator itself reads nothing from another system’s tables directly — that isolation is enforced by the access model in Security Boundaries for Traffic Database Access.
Compliance & Audit Considerations
Thresholds are a compliance surface, not just an optimization knob, so several rules are non-negotiable.
A soft tolerance may never relax a hard floor. The FCC competitive-separation minimum and, for political and issue advertising, the disclosure and lowest-unit-charge obligations are floors the calibration path is structurally forbidden from touching. In the code above this is enforced by checking separation_floor_seconds independently of every soft tolerance and by validating at profile load that a gap can never be authored above its own floor.
Every ruling is auditable and reproducible. Because the evaluator is pure and each ruling records the exact profile_version that judged the placement, an inspection can replay any historical decision and obtain a byte-identical result. That reproducibility is the evidence SOC 2 and ISO 27001 controls depend on.
Rulings are appended, never overwritten. Accepted placements, rejections, and later corrections are all written to a write-once ledger with each entry content-hashed to its predecessor. A correction is a new entry that references the original — the original is never mutated — which is what makes the schedule defensible under both FCC public-inspection and financial audit. A profile change is itself a ledgered event: the version, the operator, and the before/after tolerances are recorded so a widened tolerance can always be traced to a decision and a person.
Troubleshooting & Common Errors
| Error pattern | Root cause | Remediation |
|---|---|---|
DRIFT_RUNAWAY |
Adaptive tolerance chasing telemetry noise; drift feed reporting stale or unbounded values | Cap adjustment at max_drift_seconds, quarantine outliers beyond 3σ, and open the circuit breaker after consecutive stale heartbeats — revert to the static per-daypart profile |
SEPARATION_UNDERFLOW |
Two competing advertisers placed inside the regulatory floor, usually after a tolerance was widened to absorb an overrun | Treat as a hard failure, halt automated commit for the block, and route to a manual override queue — never suppress the flag for fill |
SATURATION_FALSE_POSITIVE |
daypart_fill_pct computed before a preemption freed inventory, so a valid spot is rejected as over-cap |
Recompute projected fill after preemption/make-good resolution, then re-evaluate the queued candidate |
LEAD_TIME_SKEW |
encoder_lead_frames rounded to whole seconds, injecting phantom sub-second drift |
Keep the frame→second conversion in floating point using the profile frame_rate; verify against playout-server latency measurements |
PROFILE_VERSION_MISMATCH |
Two subsystems loaded different profile versions, so rotation and threshold gates disagree on the same separation value | Load all tolerance parameters from one profile source of truth; fail closed if the rotation engine and evaluator report divergent profile_version |
Related
- Adjusting Thresholds for Automated Scheduling — the bounded, audit-logged calibration algorithm that widens soft tolerances in response to live schedule drift.
- Detecting Time Slot Conflicts in Traffic Logs — isolating exact collision boundaries once threshold gating has validated inter-spot spacing.
- Building Rule Engines for Spot Rotation — the rotation and separation rule sets that share this profile’s frequency and separation parameters.
- Automating Make-Good Routing for Preemptions — where placements that breach a threshold or hard floor are rebooked into a compliant window.
- Spot Scheduling Validation & Rule Engines — the parent architecture that defines the taxonomy, contracts, and failure model these thresholds operate within.