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.
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.
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:
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.