A real model, two published estimates

Apple revenue: read the export, anchor the inputs, and explain the posterior

Start with the introduction for terminology. This notebook loads real ADC model data and follows one fiscal period through both methods. Each figure comes from the loaded data and the installed library.

1. Choose the export and the question

Follow notebook setup. The export files stay where they were downloaded; no vendor client or credential is needed to read them.

  • Default export: ~/repos/adc-models/data/customers-371/line-items/190/2026-09-20.
  • Alternate location: Set POSTFORECAST_EXPORT to a directory containing universe.parquet and signals/.
  • Pinned selection: The directory, entity, target period, and as-of date are explicit below. Changing the export requires reviewing model provenance and the anchoring coefficient choice, not merely rerunning the cells.
  • Missing data: Execution stops with the missing path; synthetic data is never substituted. Existing rendered outputs remain readable without local exports.
import os
from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import tutorial_support as examples
from IPython.display import display as show

import postforecast as pf

examples.set_style()
export_root = Path(
    os.environ.get(
        "POSTFORECAST_EXPORT",
        "~/repos/adc-models/data/customers-371/line-items/190/2026-09-20",
    )
).expanduser()
ticker = "AAPL"
period = "2026-09-26"
as_of = pd.Timestamp("2026-09-22", tz="UTC")
parameters = pf.CORE_2025_01

universe = pd.read_parquet(export_root / "universe.parquet")
company = universe.loc[universe.ticker.eq(ticker) & universe.country.eq("US")].squeeze()
entity = company.entity_id
pd.DataFrame(
    {
        "setting": [
            "company",
            "ticker",
            "period end",
            "as of (exclusive UTC)",
            "export directory",
            "coefficient version",
        ],
        "value": [
            company.company_name,
            ticker,
            period,
            as_of.strftime("%Y-%m-%d"),
            export_root.name,
            parameters.version,
        ],
    }
)
setting value
0 company Apple, Inc.
1 ticker AAPL
2 period end 2026-09-26
3 as of (exclusive UTC) 2026-09-22
4 export directory 2026-09-20
5 coefficient version core-2025-01

2. Load observations, retaining their provenance

  • Live model: model_prediction supplies the current forecast.
  • Historical model: model_pit_backtest supplies reconstructed historical forecasts. Keeping it separate prevents silently splicing different source roles.
  • Consensus: The analyst reference level for the same entity and period.
  • Actual: The outcome observations available in this export, including revisions.
  • Memory: Parquet filters select one entity before loading its rows into pandas.
  • Dates: These files encode availability as dates. Parsing as UTC midnight preserves that date convention; it does not establish intraday delivery timing.

The directory’s date is a label, not a substitute for checking the timestamps inside those files.

source_names = {
    "model_prediction": "model",
    "model_pit_backtest": "model_history",
    "consensus": "consensus",
    "actual": "actual",
}
frames = []
provenance = []
for signal, source in source_names.items():
    path = export_root / "signals" / f"signal_type={signal}" / "data.parquet"
    observations = pd.read_parquet(path, filters=[("entity_id", "==", entity)])
    provenance.append({"source": source, "rows loaded": len(observations)})
    frames.append(observations.assign(source=source))
raw = pd.concat(frames, ignore_index=True)
show(pd.DataFrame(provenance))
show(
    raw.loc[
        raw.source.isin(["model", "model_history"]), ["source", "model_id"]
    ].drop_duplicates()
)
source rows loaded
0 model 22
1 model_history 339
2 consensus 10014
3 actual 17104
source model_id
0 model 303124
22 model_history 303124
ImportantA historical cutoff is not a historical model snapshot

Later exports can contain changed historical forecasts after model replacement. model_pit_backtest reconstructs history using the selected model and stored input versions. Its known_at column alone does not prove that model selection, tuning, or vendor data existed as shown on the historical date.

The calculations below respect the timestamps supplied. They demonstrate the methodology on this export, without claiming a tradable historical record.

3. Form a tidy input panel

One row means one source’s value for one subject at one availability time. The source’s fiscal period-end date becomes the opaque period label, avoiding an incorrect assumption about calendar quarters. Monetary values retain their export units throughout calculation; charts scale them to billions.

Extra provenance columns such as model_id remain attached. The panel validator rejects duplicate observation keys instead of averaging conflicting records.

# Readers know the company by its ticker, not by the vendor entity identifier.
panel = raw.rename(columns={"period_end_date": "period"}).assign(
    entity=ticker,
    target="revenue",
    period_end=pd.to_datetime(raw.period_end_date, utc=True),
    known_at=pd.to_datetime(raw.known_at, utc=True),
)
panel = pf.validate_estimates(panel)
coverage = panel.groupby("source", as_index=False).agg(
    observations=("value", "size"),
    periods=("period", "nunique"),
    first_available=("known_at", "min"),
    last_available=("known_at", "max"),
)
show(examples.format_dates(coverage))
show(
    examples.format_dates(
        panel.loc[
            panel.period.eq(period),
            ["entity", "period", "target", "source", "known_at", "value"],
        ].tail(8)
    )
)
source observations periods first_available last_available
0 actual 17104 75 2008-01-22 2026-09-13
1 consensus 10014 66 2012-10-02 2026-09-16
2 model 22 2 2026-09-10 2026-09-20
3 model_history 339 15 2023-06-05 2026-09-07
entity period target source known_at value
25957 AAPL 2026-09-26 revenue model_history 2026-07-20 111,894,639,969
25958 AAPL 2026-09-26 revenue model_history 2026-07-27 109,422,551,905
25959 AAPL 2026-09-26 revenue model_history 2026-08-03 116,314,703,478
25960 AAPL 2026-09-26 revenue model_history 2026-08-10 115,241,576,831
25961 AAPL 2026-09-26 revenue model_history 2026-08-17 114,044,294,161
25962 AAPL 2026-09-26 revenue model_history 2026-08-24 114,073,213,976
25963 AAPL 2026-09-26 revenue model_history 2026-08-31 114,566,340,305
25964 AAPL 2026-09-26 revenue model_history 2026-09-07 114,245,600,391

4. Freeze the reading moment

The first chart follows one target period through availability time. It is not a chart of revenue across quarters. A source can revise the same period many times; only its latest observation strictly before the cutoff is read.

The 30-day marker is the earlier consensus reading used for the revision. A line ending before the cutoff means the latest available observation is older than the reading moment; the library does not invent an update.

Show the code
subject_panel = panel.loc[panel.period.eq(period)]
fig, ax = plt.subplots(figsize=(10, 4.5))
for source, label in [
    ("consensus", "Consensus"),
    ("model_history", "Reconstructed model history"),
    ("model", "Live model"),
]:
    observations = subject_panel.loc[
        subject_panel.source.eq(source)
        & subject_panel.known_at.ge(as_of - pd.Timedelta(days=120))
    ].sort_values("known_at")
    ax.step(observations.known_at, observations.value / 1e9, where="post", label=label)
ax.axvline(as_of, color="black", linestyle="--", label="As-of cutoff")
ax.axvline(
    as_of - pd.Timedelta(days=30),
    color="gray",
    linestyle=":",
    label="Revision lookback",
)
ax.set(
    xlabel="Availability date (UTC)",
    ylabel="Revenue (export units, billions)",
    title=f"{ticker}: fiscal period ending {period}",
)
ax.legend(loc="best")
fig.autofmt_xdate()
plt.show()
Model and consensus across availability dates with cutoff markers.
Figure 1: One target period, two reading moments.
snapshot = pf.latest_before(subject_panel, as_of)
show(examples.format_dates(snapshot[["source", "known_at", "value"]]))
subjects = pf.prepare_anchored_subjects(
    panel,
    as_of,
    forecast_source="model",
    subjects=subject_panel[
        ["entity", "period", "target", "period_end"]
    ].drop_duplicates(),
)
show(
    examples.format_dates(
        subjects[
            [
                "period",
                "forecast",
                "forecast_known_at",
                "forecast_age",
                "consensus",
                "consensus_known_at",
                "consensus_age",
                "consensus_revision",
            ]
        ]
    )
)
source known_at value
0 consensus 2026-09-16 113,563,745,165
1 model 2026-09-20 113,643,875,999
2 model_history 2026-09-07 114,245,600,391
period forecast forecast_known_at forecast_age consensus consensus_known_at consensus_age consensus_revision
0 2026-09-26 113,643,875,999 2026-09-20 2 days 113,563,745,165 2026-09-16 6 days 0.00632042

5. Explain the consensus-anchored estimate

Let \(m\) be the model, \(c\) consensus, and \(c_{-30}\) the consensus read thirty days earlier, as on the notation page.

\[ \Delta=\frac{m-c}{\lvert c\rvert},\qquad r=\frac{c}{c_{-30}}-1 \]

\[ \hat s=a+b_\Delta\,\Delta+b_r\,r,\qquad \hat y=c\,(1+\hat s) \]

  • Customary beat: \(a\), the fitted intercept.
  • Model contribution: the gap weight \(b_\Delta\) times the current gap \(\Delta\).
  • Revision contribution: the revision weight \(b_r\) times the consensus revision \(r\).
  • Frozen coefficients: CORE_2025_01 is the shipped Core example, trained through 2024-12-31. Applying it to this later export demonstrates the arithmetic; it does not validate those coefficients for the export’s changed model vintage.
  • Missing earlier consensus: The method explicitly drops the revision term when no revision can be measured. Other missing required inputs can cause refusal.

The contribution chart measures percentage points of expected surprise, not revenue dollars. The contributions must sum to the published surprise \(\hat s\).

anchoring = pf.AnchoredEstimate(forecast_source="forecast", parameters=parameters).fit(
    subjects
)
anchored = anchoring.apply(subjects)
show(
    anchored[
        [
            "period",
            "anchored_level",
            "expected_surprise",
            "eligible",
            "eligibility_reason",
            "parameters_version",
        ]
    ]
)
contributions = anchored.iloc[0][
    ["contribution_customary_beat", "contribution_gap", "contribution_revision"]
].astype(float)
contributions.index = ["Customary beat", "Model gap", "Consensus revision"]
contributions.mul(100).to_frame("Contribution (percentage points)")
period anchored_level expected_surprise eligible eligibility_reason parameters_version
0 2026-09-26 115,464,307,061 0.0167356 True publishable core-2025-01
Contribution (percentage points)
Customary beat 0.89
Model gap 0.00311876
Consensus revision 0.780445
Show the code
fig, ax = plt.subplots(figsize=(9, 3.6))
contributions.mul(100).plot.barh(
    ax=ax, color=[examples.MUTED, examples.ACCENT, examples.WARM]
)
ax.axvline(0, color="black", linewidth=0.8)
ax.set(xlabel="Contribution to expected surprise (percentage points)", ylabel="")
plt.show()
Bars for customary beat, model gap, and consensus revision.
Figure 2: Three contributions sum to expected surprise.

6. Build the independent Bayesian posterior

The Bayesian route learns uncertainty from the company’s realised history. Historical model and consensus readings are selected strictly before each period’s first actual timestamp present in the export. Forward moments use only actuals available before the outer as_of moment.

  • First observed actual: This notebook selects the first actual in the export before preparation. The posterior API otherwise uses the latest known actual. If earlier original reports are absent, that is a provenance gap; later data cannot repair it by relabelling a timestamp.
  • Prior growth: Mean \(\mu_0\) and sample variance \(\sigma_0^2\) of sequential realised growth \(g_i\).
  • Model uncertainty: Mean squared historical model growth error, \(\sigma_M^2\).
  • Consensus uncertainty: Mean squared historical consensus growth error, \(\sigma_C^2\).
  • Separate histories: The notebook uses reconstructed forecasts for earlier periods and the live model for the target period, then explicitly names that selected series model for posterior preparation. Original source labels remain recorded in the input panel.
  • Basic configuration: Bias correction and analyst-dispersion weighting stay off so every weight is explained by the historical errors. The posterior reference covers those extensions.

The same previous actual \(y_{t-1}\) converts every reading to growth: \(x_M=m/y_{t-1}-1\) and \(x_C=c/y_{t-1}-1\). With precisions \(\tau_j=1/\sigma_j^2\), the weights \(w_j\) and the posterior growth \(\hat\theta\) are:

\[ w_j=\frac{\tau_j}{\tau_0+\tau_M+\tau_C},\qquad \hat\theta=w_0\mu_0+w_Mx_M+w_Cx_C. \]

The posterior level is \(\hat y=y_{t-1}(1+\hat\theta)\).

settings = pf.posterior_preset("plain-2026-09")
first_actuals = (
    panel.loc[panel.source.eq("actual")]
    .sort_values("known_at")
    .drop_duplicates(["entity", "period", "target"])
)
selected_models = panel.loc[
    (
        panel.source.eq("model_history")
        & panel.period_end.lt(pd.Timestamp(period, tz="UTC"))
    )
    | (panel.source.eq("model") & panel.period.eq(period))
].assign(source="model")
posterior_panel = pd.concat(
    [panel.loc[panel.source.eq("consensus")], first_actuals, selected_models],
    ignore_index=True,
)
prepared = pf.prepare_bayesian_subjects(
    posterior_panel,
    as_of,
    forecast_source="model",
    settings=settings,
)
bayesian = pf.BayesianPosterior(forecast_source="model", settings=settings)
posteriors = bayesian.fit(prepared).apply(prepared)
posterior = posteriors.loc[posteriors.period.eq(period)].iloc[0]
ingredients = pd.DataFrame(
    {
        "Ingredient": ["Prior", "Model", "Consensus"],
        "Growth": [
            posterior.prior_growth_mean,
            posterior.model / posterior.previous_actual - 1,
            posterior.adjusted_consensus / posterior.previous_actual - 1,
        ],
        "Precision": [
            posterior[f"precision_{name}"] for name in ["prior", "model", "consensus"]
        ],
        "Weight": [
            posterior[f"weight_{name}"] for name in ["prior", "model", "consensus"]
        ],
    }
)
ingredients["Weighted growth"] = ingredients.Growth * ingredients.Weight
show(ingredients)
show(
    posteriors.loc[
        posteriors.period.eq(period),
        ["period", "posterior_level", "posterior_lower", "posterior_upper", "eligible"],
    ]
)
Ingredient Growth Precision Weight Weighted growth
0 Prior 0.0692899 11.9979 0.0184346 0.00127733
1 Model 0.0386309 187.554 0.288174 0.0111324
2 Consensus 0.0378985 451.285 0.693391 0.0262785
period posterior_level posterior_lower posterior_upper eligible
75 2026-09-26 113,650,155,095 105,754,139,535 123,004,405,835 True
Show the code
fig, axes = plt.subplots(1, 2, figsize=(11, 4))
colors = [examples.MUTED, examples.ACCENT, examples.WARM]
axes[0].bar(ingredients.Ingredient, ingredients.Growth * 100, color=colors)
axes[0].axhline(
    posterior["posterior_growth"] * 100,
    color="black",
    linestyle="--",
    label="Posterior",
)
axes[0].set(ylabel="Sequential growth (%)", title="What each ingredient says")
axes[0].legend()
axes[1].bar(ingredients.Ingredient, ingredients.Weight * 100, color=colors)
axes[1].set(ylabel="Weight (%)", ylim=(0, 100), title="How much weight it receives")
fig.tight_layout()
plt.show()
Growth readings and posterior weights for the three ingredients.
Figure 3: Estimated precision determines each weight.

7. Compare the answers in revenue units

  • Anchored estimate: The frozen rule expresses expected surprise relative to consensus. Its intercept and revision term can place it outside the gap between the model and consensus.
  • Bayesian posterior: The independent precision-weighted combination operates in growth space and includes the historical prior as a third ingredient.
  • Interval: The displayed band uses the plain preset’s fitted Student-t shape. Its research training cutoff is unknown; see preset provenance. A broad interval exposes uncertainty; it is not evidence of measured 90% coverage on this company.
  • No winner inferred: This single example cannot establish which method is more accurate. That requires a separate, provenance-aware evaluation.
Show the code
levels = pd.Series(
    {
        "Consensus": subjects.consensus.iloc[0],
        "Model": subjects.forecast.iloc[0],
        "Anchored estimate": anchored.anchored_level.iloc[0],
        "Bayesian posterior": posterior["posterior_level"],
    }
)
fig, ax = plt.subplots(figsize=(10, 4))
ax.scatter(levels / 1e9, levels.index, s=65, zorder=3)
ax.hlines(
    "Bayesian posterior",
    posterior["posterior_lower"] / 1e9,
    posterior["posterior_upper"] / 1e9,
    color=examples.ACCENT,
    linewidth=3,
)
ax.set(
    xlabel="Revenue (export units, billions)",
    title=f"{ticker}: {period}, read as of {as_of:%Y-%m-%d}",
)
plt.show()
Four revenue estimates and the Bayesian uncertainty interval.
Figure 4: Two methods applied to the same current readings.
levels.div(1e9).rename("Revenue (export units, billions)").round(3).to_frame()
Revenue (export units, billions)
Consensus 113.564
Model 113.644
Anchored estimate 115.464
Bayesian posterior 113.65

8. Inspect what cannot be published

Farther-out periods may have consensus but no live model. The output retains those subjects and their reasons. A diagnostic or a consensus value alone is not permission to treat a refused prediction as published.

Removing the live forecast demonstrates an anchored refusal on the same subject. The original calculation remains unchanged.

show(posteriors[["period", "posterior_level", "eligible", "eligibility_reason"]])
without_model = panel.loc[panel.source.ne("model")]
refused = pf.publish_anchored(
    without_model,
    as_of,
    forecast_source="model",
    parameters=parameters,
    subjects=subject_panel[
        ["entity", "period", "target", "period_end"]
    ].drop_duplicates(),
)
show(refused[["period", "expected_surprise", "eligible", "eligibility_reason"]])
period posterior_level eligible eligibility_reason
0 2007-12-29 NaN False missing_input
1 2008-03-29 NaN False missing_input
2 2008-06-28 NaN False missing_input
3 2008-09-27 NaN False missing_input
4 2008-12-27 NaN False missing_input
... ... ... ... ...
81 2028-04-01 NaN False missing_prediction
82 2028-07-01 NaN False missing_prediction
83 2028-09-30 NaN False missing_prediction
84 2028-12-30 NaN False missing_prediction
85 2029-03-31 NaN False missing_prediction

86 rows × 4 columns

period expected_surprise eligible eligibility_reason
0 2026-09-26 NaN False missing_input

9. Check the calculation and keep the record

These executable checks verify the arithmetic and that filtering out observations at or after as_of leaves the Bayesian result unchanged. They cannot establish upstream historical availability; that requires the model and vendor provenance.

The audit payload records method settings and rows loaded per source. Store it alongside the input snapshot and the exact installed postforecast revision in the consuming application.

np.testing.assert_allclose(contributions.sum(), anchored.expected_surprise.iloc[0])
np.testing.assert_allclose(ingredients.Weight.sum(), 1.0)
np.testing.assert_allclose(
    ingredients["Weighted growth"].sum(), posterior["posterior_growth"]
)
np.testing.assert_equal(bool(snapshot.known_at.lt(as_of).all()), desired=True)
np.testing.assert_equal(bool(refused.eligible.any()), desired=False)
np.testing.assert_equal(bool(refused.expected_surprise.isna().all()), desired=True)
trimmed_subjects = pf.prepare_bayesian_subjects(
    posterior_panel.loc[posterior_panel.known_at.lt(as_of)],
    as_of,
    forecast_source="model",
    settings=settings,
)
trimmed = bayesian.fit(trimmed_subjects).apply(trimmed_subjects)
pd.testing.assert_frame_equal(posteriors, trimmed)
audit_record = {
    "export": export_root.name,
    "entity": entity,
    "period": period,
    "as_of": as_of.isoformat(),
    "source_files": provenance,
    "anchoring": anchoring.to_dict(),
    "bayesian": bayesian.fit(prepared).to_dict(),
}
print("Checks passed: attribution, weights, posterior, cutoff, and refusal.")
Checks passed: attribution, weights, posterior, cutoff, and refusal.

Try the methodology yourself

  1. Move the reading moment: Choose an earlier as_of within the live-model coverage and rerun. Observe which timestamps are selected and whether the freshness rules still permit publication.
  2. Change the target: Use another fiscal period visible in the output. Keep all historical input periods so the posterior retains its training history.
  3. Raise the history threshold: Increase minimum_observations and inspect eligibility, missing weights, and intervals. The posterior still combines prior and consensus when model precision is unavailable; read that rule before interpreting such a result.
  4. Inspect a different model: Change the ticker and select a matching fiscal period. Confirm coverage and units before comparing the result.
  5. Go deeper: Follow Anchoring, Bayesian posterior, and the posterior reference.

A production caller additionally decides which export vintage, source freshness policy, parameter calibration, and interval validation are appropriate for its use. The Bayesian API does not inherit the anchored method’s age limits automatically.