Inverting the calibration at the posterior-median parameters is not the same as the median of the full predict_T_from_proxyObs posterior — and for the TEXAS S-curve the gap is largest exactly where you care most: the temperature extremes.
TEXAS reconstructs temperature by inverting a generalized-logistic calibration
scaledRI = b + (1−b)/(1+e^(−k(T−t0)))^(1/v) + ε, marginalizing over ~100 forward-posterior draws and applying a temperature prior T ~ TruncNormal(μ, σ, lower=min_temp). Your "frequentist" estimate inverts that curve once, at the median (t0, k, b, v). Three things make the two P50s disagree — and near the S-curve's flat ends the plug-in doesn't just disagree, it breaks. Everything below runs on the real univ_priorApprox_SST_scaledRI_cren3 posterior.
The full posterior is a prior × marginalized-likelihood compromise over a steeply nonlinear inverse. The plug-in ignores the prior, ignores the ensemble mixture, and inverts a single S-curve that misbehaves near its own asymptote b≈0.42. Result: a systematic ~1–2 °C offset mid-curve, ballooning to 5–15 °C (or undefined) at the cold and warm extremes.
The inverse t0 − ln(((1−b)/(y−b))^v − 1)/k explodes as the proxy approaches b (lower) or 1 (upper). At the median curve, a proxy just above b maps to an absurd cold value; a proxy below the median b is undefined. The marginalized posterior stays finite because most ensemble members have a lower b. Dominant at temperature extremes.
TEXAS averages the likelihood over ~100 forward draws (the S-curves fan out — steepness k and shape v are very uncertain). Because the inverse is steeply nonlinear, medianm[inv(y;θ_m)] ≠ inv(y;θ̂) — this moves the median, not just the mean. Dominant on the steep flanks.
Where the data are informative (steep middle of the S-curve), the posterior is a precision-weighted blend of the proxy-implied T and the prior mean, shrinking toward μ by ~1–2 °C for a diffuse σ=10 and much more for a tight prior. The plug-in uses none of it. Widen σ in the sandbox and this piece vanishes — the clean diagnostic that it's the prior, not arithmetic.
Left: the real forward calibration — 80 posterior S-curves (thin blue), the median curve (bold), the lower asymptote b, and your proxy read across to the plug-in temperature. Right: the marginalized temperature posterior (blue) against the prior (violet) and the plug-in point (orange). Push the proxy toward b≈0.42 or toward 1 and watch the plug-in detonate.
scaledRI vs temperature — real posterior ensemble; proxy read across to the plug-in T
marginalized posterior (blue) vs prior (violet) vs plug-in point (orange)
Same proxy, built up in stages — relative to the plug-in point.
| Source added | |median gap| |
|---|
p50 from predict_T_from_proxyObs already marginalizes the ensemble and applies the prior — that's the number to plot. The plug-in is only a sanity shortcut, and an unreliable one near the asymptotes.The correct call, and the grid check, in your own API:
from TEXAS.predict import predict_T_from_proxyObs
res = predict_T_from_proxyObs(
proxyObs=ri, prior_mu_t=15.0, prior_sigma_t=10.0, min_temp=-1.8, # truncated_prior auto
fwd_posterior="gen_logi_fixed_hier_crtp_univ_priorApprox_SST_scaledRI_cren3",
temptype="SST")
res["p50"] # ✓ marginalized + prior-aware median
# fast grid check (no Stan) — matches res["p50"]:
def fwd(T,t0,k,b,v): return b + (1-b)/np.power(1+np.exp(-k*(T-t0)), 1/v)
T = np.linspace(-1.8, 45, 4000)
like = np.mean([np.exp(-0.5*((ri-fwd(T,*p[:4]))/p[4])**2)/p[4] for p in draws], axis=0)
post = like * np.exp(-0.5*((T-15)/10)**2) # × T prior
cdf = np.cumsum(post); t_p50 = np.interp(0.5*cdf[-1], cdf, T)
# plug-in (what you did) — ignores prior + mixture, undefined for ri < b_med:
from TEXAS.models.logistics import inverse_generalized_logistic_fixed_upper
t_bad = inverse_generalized_logistic_fixed_upper(ri, t0=t0_med, b=b_med, k=k_med, v=v_med)