Point-in-time

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

The as-of join is the whole discipline, and it is easy to get subtly wrong

In the study this package was extracted from, one join de-duplicated on the event key alone and broadcast a single formation day’s signal across the whole holding window. 580 of 586 daily returns changed. Every unit test still passed. Only a comparison against the original scripts caught it.

That is why there is one shared anchoring primitive here instead of careful joins in ten places, and why a method never sees the raw panel.

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

estimates = pfc.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,
            ),
        }
    )
)
estimates
entity period target period_end source value known_at
0 AAA 2026Q1 revenue 2026-03-31 00:00:00+00:00 consensus 99.0 2025-11-01 00:00:00+00:00
1 AAA 2026Q1 revenue 2026-03-31 00:00:00+00:00 consensus 100.0 2026-01-15 00:00:00+00:00
2 AAA 2026Q1 revenue 2026-03-31 00:00:00+00:00 model 105.0 2025-10-01 00:00:00+00:00
3 AAA 2026Q1 revenue 2026-03-31 00:00:00+00:00 model 110.0 2026-01-12 00:00:00+00:00
4 AAA 2026Q1 revenue 2026-03-31 00:00:00+00:00 model 118.0 2026-01-20 00:00:00+00:00

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
pd.concat(
    [
        pfc.latest_before(estimates, AS_OF).assign(read="strictly before (default)"),
        pfc.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.0 2026-01-15 00:00:00+00:00
1 strictly before (default) model 110.0 2026-01-12 00:00:00+00:00
0 inclusive consensus 100.0 2026-01-15 00:00:00+00:00
1 inclusive model 118.0 2026-01-20 00:00:00+00:00

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
pfc.latest_before(estimates, AS_OF, maximum_age=pd.Timedelta(days=30))[
    ["source", "value", "known_at"]
]
source value known_at
0 consensus 100.0 2026-01-15 00:00:00+00:00
1 model 110.0 2026-01-12 00:00:00+00:00

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 = pfc.age_at(pfc.pivot_sources(pfc.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.0 100.0 8 days 5 days

Age arrives as a value. A feed consumer can act on the difference: a stale model means fix the model pipeline, a missing one means the company is not covered.

fit learns, apply does not

Look-ahead arrives through convenience

The legacy pre-report analysis winsorised the full sample before splitting it, so a test-period outlier moved a training-period bound. Nobody intended it. It was one line in the wrong order.

Hence two types. fit takes the training rows and returns a frozen object. apply takes any rows and cannot refit. A look-ahead has to be written deliberately.

Show the code
generator = np.random.default_rng(7)
size = 400
consensus = generator.uniform(50.0, 5000.0, size)
everything = pd.DataFrame(
    {
        "entity": [f"E{index:04d}" for index in range(size)],
        "period": ["2026Q1"] * size,
        "target": ["revenue"] * size,
        "period_end": pd.to_datetime(["2026-03-31"] * size, utc=True),
        "consensus": consensus,
        "model": consensus * (1 + generator.uniform(-0.3, 0.3, size)),
        "consensus_revision": generator.uniform(-0.05, 0.05, size),
        "model_age": pd.to_timedelta(generator.integers(0, 20, size), unit="D"),
        "consensus_age": pd.to_timedelta(generator.integers(0, 50, size), unit="D"),
    }
)
training = everything.iloc[: size // 2]

method = pfc.AnchoredEstimate(forecast_source="model")
from_training = method.fit(training).apply(everything)
from_everything = method.fit(everything).apply(everything)

from_training.equals(from_everything)
True

True, and it has to stay True. Every fitted method carries a test that proves it: fitting on the training slice alone gives the same answer as fitting on everything. A method that peeks fails it.

The anchored estimate is a frozen-parameter method — its coefficients were fitted offline and shipped with a version — so fit verifies the panel and learns nothing at all. That is the strongest version of the same guarantee.

Degenerate input

Each of these crashed the study before it was fixed at the root, and each is a test here now.

Show the code
empty = pfc.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 = pfc.AnchoredEstimate(forecast_source="model").fit(zero).apply(zero)
print(
    "a zero denominator yields a refusal, never an infinity:",
    refused[pfc.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']

The rules, in one list

  • Strictly before. The inclusive read is opt-in and named.
  • Maximum age is part of the join when you are choosing a value, and a value when a method has to explain itself.
  • First reported, never restated. An actual revised since the event is not what the market saw.
  • Anchor before reading. A method sees the anchored frame, never the raw panel, and the reading moment is always explicit.
  • fit learns, apply does not, and there is a test for it.
  • Publish nothing rather than a fallback. A stale input and a real number must never become indistinguishable downstream.

Next: the first method.