Consensus, moved a fraction of the way toward the forecast
The customer’s question is never “what will revenue be?” — they already have consensus. It is “where is consensus wrong?” This page builds the number that answers it, and shows every step with real figures.
NoteProvenance
Ported from docs/anchoring.qmd and docs/anchoring_signal.py in the buy-side consensus repository, from one company-quarter of scalars to the panel API. That reference ends in a self-check with real numbers, and the self-check is a golden test here, so this page and the published research cannot drift apart.
Step 1 — the inputs, and when they may be used
Four inputs per company-quarter, each read as of the moment the number is published: the value whose known_at is the latest one strictly before that instant.
Five steps, every one explicit: anchor, reshape, age, measure the revision, join it on. A method never reaches past this pipeline to the raw panel.
Step 3 — the three ingredients
All three are relative quantities, so companies of any size are comparable.
\[
g = \frac{\text{model}}{\text{consensus}} - 1
\qquad\text{the gap: what the data says consensus is missing}
\]
\[
r = \frac{\text{consensus}_t}{\text{consensus}_{t-30}} - 1
\qquad\text{the revision: what analysts have already started to do}
\]
\[
s = \frac{\text{actual}}{\text{consensus}} - 1
\qquad\text{the surprise: the target, known only afterwards}
\]
The revision is two anchored reads of the same quantity, thirty days apart, which is why it lives in history and not in the method. A missing prior consensus comes back missing, never as a revision of zero — those are different facts.
Step 4 — fit the weights once, and freeze them
The published estimate is a forecast of the surprise from the gap and the revision:
\[
\hat s = a + b_g\, g + b_r\, r
\]
Fitted by ordinary least squares on reports before a cut-off date, then never refitted inside the evaluation period. The library ships the fitted sets as data:
This is the step that surprises people. Let \(\theta\) be the part of the surprise that is genuinely knowable in advance. The gap is a noisy reading of it, and the surprise is \(\theta\) plus what nobody could have known:
\[
g = \theta + \eta, \qquad s = \theta + \epsilon
\]
A signal-to-total-variance ratio, always between 0 and 1. A weight of 0.129 does not mean the model is usually wrong. It means the gap’s noise variance is about seven times the variance of its predictive part.
Figure 1: Simulated from the attenuation formula with the study’s noise ratio. The cloud is wide, the line is shallow, and the quintile sort is still monotone — which is the whole business case.
Why \(b_r\) is above 1
A coefficient above one looks wrong and is not. Analysts revise toward the truth but stop short, so the observed revision is a partial adjustment. The regression recovers the structural weight divided by the share analysts complete. A weight of 1.28 implies they complete roughly three-quarters of the move before the report.
Step 5 — anchor
Do not publish the expected surprise as a level on its own. Multiply consensus by it:
\[
\hat y = c \times (1 + \hat s)
\]
Substituting the gap shows what anchoring really is:
\[
\hat y = \underbrace{c\,(1 + a + b_r r)}_{\text{consensus, corrected}}
+ \underbrace{b_g\,(m - c)}_{\text{a fraction of the disagreement}}
\]
The three contributions sum to the expected surprise. That is what makes the number attributable: a customer sees which part of the adjustment is alternative data and which is consensus dynamics, and each data partner sees its own contribution.
Figure 2: Where the published number sits, for the worked example.
The published number inherits the accuracy of consensus, and every bit of the alternative data sits in the difference.
Step 6 — publish nothing rather than a fallback
A feed that fills a gap with consensus is publishing “no information” as if it were a forecast. A feed that drops the row silently makes the gap invisible. So the row stays, the numbers go missing, and the reason travels with it.
The check order is part of the contract: missing input, then stale model, then stale consensus, then wide gap. A subject that is both stale and wide-gapped reports the staleness, because that is the fault to fix.
A gap beyond 25% is almost always a mismatched fiscal period or a restatement rather than information, which is why it is a data break and not a very strong signal.
Step 7 — version every value
Fitted coefficients become constants shipped with the signal, re-estimated on a schedule and never tuned between releases. A consumer who backtests your history needs to know which coefficient set produced each published value.
Show the code
pd.DataFrame( [ {"version": parameters.version,"fitted_through": str(parameters.fitted_through),"expected_surprise": f"{out.expected_surprise:+.4%}","anchored_level": f"{out.anchored_level:,.2f}", }for parameters in (pfc.STUDY_2025_01, pfc.CORE_2025_01)for out in [ pfc.AnchoredEstimate(forecast_source="model", parameters=parameters) .fit(subjects) .apply(subjects) .iloc[0] ] ])
version
fitted_through
expected_surprise
anchored_level
0
study-2025-01
2024-12-31
+3.2968%
825.13
1
core-2025-01
2024-12-31
+2.5228%
818.95
Same inputs, different published number, and the difference is explainable rather than mysterious. The record a run writes down carries both: