The panel

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.

Show the code
estimates = pf.example_panel()
pfc.validate_estimates(estimates)
entity period target period_end source value known_at
0 ZS 2026Q1 revenue 2026-01-31 00:00:00+00:00 consensus 790.0 2025-12-01 00:00:00+00:00
1 ZS 2026Q1 revenue 2026-01-31 00:00:00+00:00 consensus 798.8 2026-01-15 00:00:00+00:00
2 ZS 2026Q1 revenue 2026-01-31 00:00:00+00:00 model 845.3 2026-01-12 00:00:00+00:00

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.

Show the code
broken = estimates.assign(
    value=["1", "2", "3"],
    known_at=["last year", "last month", "last week"],
)
try:
    pfc.validate_estimates(broken)
except pfc.PanelError as refusal:
    print(refusal)
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.

Show the code
anchored = pfc.latest_before(
    pfc.validate_estimates(estimates), pd.Timestamp("2026-01-20", tz="UTC")
)
pfc.pivot_sources(anchored)
entity period target period_end consensus model consensus_known_at model_known_at
0 ZS 2026Q1 revenue 2026-01-31 00:00:00+00:00 798.8 845.3 2026-01-15 00:00:00+00:00 2026-01-12 00:00:00+00:00

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.

Next: how that read is made honest.