One row per estimate, and what the validator refuses
Everything in the library starts from one long frame. One row is one number, said by one source, about one subject, knowable from one moment. Long rather than wide, so three forecasters are the same code path as one.
The columns
Column
Type
Meaning
entity
str
Company or series identifier
period
str
The fiscal period label; opaque to the library
period_end
datetime, UTC-aware
When that period ends; orders periods and drives the age rules
target
str
The quantity forecast, such as revenue
source
str
consensus, actual, or any forecaster name
value
float
The number
known_at
datetime, UTC-aware
When it became knowable; the anchor for everything
estimate_count
float, optional
Contributor count behind a consensus
dispersion
float, optional
Standard deviation across contributors
Two source names carry meaning, consensus and actual. Every other name is a forecaster, opaque to the library and named by the caller.
The validator returns the panel in canonical order — sorted by subject, source and known_at, with a fresh index — so downstream code never depends on the order a caller happened to build it in.
Every timestamp is UTC-aware
known_at and period_end must carry UTC. Naive timestamps and other zones are refused, and never coerced: a panel is joined across sources and venues, and silently converting a naive timestamp would be guessing which zone the caller meant.
Show the code
try: pfc.validate_estimates( estimates.assign(known_at=estimates.known_at.dt.tz_localize(None)) )except pfc.PanelError as refusal:print(refusal)
estimate panel is not valid: known_at must be tz-aware UTC, not naive or another zone. A panel is joined across sources and venues, and an unlabelled timestamp leaves the anchoring order dependent on whoever produced the row.
The contract is a schema, not a pile of checks
The format is declared as a Pandera schema and validated lazily, so one call reports every way a panel is malformed rather than stopping at the first. A caller fixing a panel gets the whole list.
estimate panel is not valid: known_at must be a datetime column. A string column silently compares lexically, which is not the ordering anchoring needs.; value must be numeric.
Two failures, one message. The failures come back as a PanelError — the domain error type for this boundary — so a caller never learns which validation library is underneath.
What else it refuses, and why
Show the code
cases = {"a subject that ends on two different dates": estimates.assign( period_end=pd.to_datetime( ["2026-01-31", "2026-04-30", "2026-01-31"], utc=True ) ),"one source revising twice at the same instant": pd.concat( [estimates, estimates.iloc[[1]]], ignore_index=True ),"a row that cannot be placed in time": estimates.assign( known_at=pd.to_datetime([None, "2026-01-15", "2026-01-12"], utc=True) ),}for description, frame in cases.items():try: pfc.validate_estimates(frame)except pfc.PanelError as refusal:print(f"{description}:\n{refusal}\n")
a subject that ends on two different dates:
estimate panel is not valid: one subject carries more than one period_end. A subject ends once, and the age rules read that date.
one source revising twice at the same instant:
estimate panel is not valid: a source's estimate for one subject repeats at one instant. Two values knowable at the same moment leave the anchoring order undefined.
a row that cannot be placed in time:
estimate panel is not valid: a column that must be complete has a missing value in it.; a row is missing one of entity, period, target, source, known_at. A row that cannot be placed in time or attributed to a source cannot be anchored.
Each refusal is there because the alternative is worse:
Two period ends for one subject would make the age rules read a different date depending on which row was consulted.
Two values from one source at one instant leave the anchoring order undefined. There is no correct answer to “which was later”.
A missing key or timestamp means a row that cannot be placed in time or attributed to a source, and therefore cannot be anchored at all.
One row per subject, which is what a method reads
Methods read a subject at a time: the forecast, the consensus and, when it exists, the actual, side by side.
A value column per source, and a known_at column per source. period_end travels with the subject, so a method can reason about the period without going back to the panel.