Point-in-time

The read itself, and why fit and apply are different types

Read observations at an explicit cutoff

A forecast can change several times for one target period. The observation’s known_at decides when it can be used; its period_end does not. The example below contains a model revision exactly at the reading cutoff.

Show the code
AS_OF = pd.Timestamp("2026-01-20", tz="UTC")

estimates = pf.validate_estimates(
    pd.DataFrame(
        {
            "entity": ["AAA"] * 5,
            "period": ["2026Q1"] * 5,
            "target": ["revenue"] * 5,
            "period_end": pd.to_datetime(["2026-03-31"] * 5, utc=True),
            "source": ["model", "model", "model", "consensus", "consensus"],
            "value": [105.0, 110.0, 118.0, 99.0, 100.0],
            "known_at": pd.to_datetime(
                ["2025-10-01", "2026-01-12", "2026-01-20", "2025-11-01", "2026-01-15"],
                utc=True,
            ),
        }
    )
)
examples.format_dates(estimates)
entity period target period_end source value known_at
0 AAA 2026Q1 revenue 2026-03-31 consensus 99 2025-11-01
1 AAA 2026Q1 revenue 2026-03-31 consensus 100 2026-01-15
2 AAA 2026Q1 revenue 2026-03-31 model 105 2025-10-01
3 AAA 2026Q1 revenue 2026-03-31 model 110 2026-01-12
4 AAA 2026Q1 revenue 2026-03-31 model 118 2026-01-20

Strictly before, not at or before

The model revision at 2026-01-20 became knowable at exactly the reading moment. The default excludes it.

Show the code
examples.format_dates(
    pd.concat(
        [
            pf.latest_before(estimates, AS_OF).assign(read="strictly before (default)"),
            pf.latest_before(estimates, AS_OF, inclusive=True).assign(read="inclusive"),
        ]
    )[["read", "source", "value", "known_at"]]
)
read source value known_at
0 strictly before (default) consensus 100 2026-01-15
1 strictly before (default) model 110 2026-01-12
0 inclusive consensus 100 2026-01-15
1 inclusive model 118 2026-01-20

An event’s own timestamp is the common reading moment, and a value knowable at that instant is usually the value under study. Including it by default is how a backtest quietly reads its own answer. The inclusive case exists, and it is opt-in and named.

Maximum age is part of the join, not a later filter

A forecast nobody has revised in a year is not a current view. Carrying a frozen value forward turns silence into a signal.

Show the code
examples.format_dates(
    pf.latest_before(estimates, AS_OF, maximum_age=pd.Timedelta(days=30))[
        ["source", "value", "known_at"]
    ]
)
source value known_at
0 consensus 100 2026-01-15
1 model 110 2026-01-12

The caps are research decisions, so they are arguments, never constants buried in the library.

Staleness has to survive the read

Note what just happened above: the stale model disappeared. That is the right answer when you are choosing a value, and the wrong answer when a method has to report why a subject is unpublishable — the row vanishes, and “stale prediction” collapses into “missing input”.

So the pipeline that feeds a method anchors without a cap and measures age instead:

Show the code
subjects = pf.age_at(pf.pivot_sources(pf.latest_before(estimates, AS_OF)), AS_OF)
subjects[["entity", "model", "consensus", "model_age", "consensus_age"]]
entity model consensus model_age consensus_age
0 AAA 110 100 8 days 5 days

Age arrives as a value. Anchored eligibility uses its configured age limits; BayesianPosterior leaves the freshness decision to the application.

Preparation establishes the historical boundary

BayesianPosterior.fit validates the prepared input; it does not learn across subjects. Its history was already computed by prepare_bayesian_subjects. A frozen object does not prevent look-ahead if the caller prepared that history at the wrong cutoff. For a replay, prepare each target at its own reading time.

Show the code
history = examples.quarterly_history()
reading_time = pd.Timestamp("2026-05-10", tz="UTC")
settings = pf.posterior_preset("plain-2026-09")
full = pf.prepare_bayesian_subjects(
    history,
    reading_time,
    forecast_source="model",
    settings=settings,
)
known_only = pf.prepare_bayesian_subjects(
    history.loc[history.known_at.lt(reading_time)],
    reading_time,
    forecast_source="model",
    settings=settings,
)
pd.testing.assert_frame_equal(full, known_only)
print("Future observations do not change the prepared history.")
Future observations do not change the prepared history.

The check compares preparation with and without future rows. It tests the reading boundary, not whether source timestamps or a calibration’s training date are historically correct. See preset provenance.

Empty inputs and unusable values

empty = pf.latest_before(estimates.iloc[:0], AS_OF)
print("empty panel comes back typed and empty:", empty.shape, list(empty.columns))

zero = subjects.assign(consensus=0.0, consensus_revision=0.0)
refused = pf.AnchoredEstimate(forecast_source="model").fit(zero).apply(zero)
print(
    "a zero denominator yields a refusal, never an infinity:",
    refused[pf.ELIGIBILITY_REASON_COLUMN].tolist(),
)
empty panel comes back typed and empty: (0, 7) ['entity', 'period', 'target', 'period_end', 'source', 'value', 'known_at']
a zero denominator yields a refusal, never an infinity: ['missing_input']

Apply the policy explicitly

  • Reading cutoff: Preparation excludes observations at or after as_of.
  • Freshness: Retain ages when the publication policy needs to explain staleness.
  • Actual revisions: Posterior preparation reads the latest known actual. Select first releases beforehand when the application requires that policy.
  • Historical replay: Recompute preparation at each historical reading cutoff.
  • Provenance: Record model vintage and calibration cutoff separately from observation times.
  • Refusals: Keep the subject’s reason instead of substituting a number.

Continue with Bayesian posterior or Anchoring.