Why the plug-in P50 doesn't match the TEXAS Bayesian P50

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 short answer

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.

Three effects, and where each dominates

S-curve inverse breaks down near the asymptotes

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.

Marginalizing a mixture of curves ≠ inverting the median curve

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.

3 · The temperature prior — the systematic mid-curve offset

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.

Drive it yourself

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.

Forward calibration & inverse

scaledRI vs temperature — real posterior ensemble; proxy read across to the plug-in T

Reconstructed temperature

marginalized posterior (blue) vs prior (violet) vs plug-in point (orange)

T prior posterior plug-in P50 Bayes P50
Plug-in P50 (invert median curve)
TEXAS Bayesian P50
°C
Disagreement (Bayes − plug-in)
Lower asymptote is b≈0.42. Approach it (or 1.0) to see the plug-in inverse blow up or go undefined.
Where the prior pulls. The mid-curve posterior shrinks from the plug-in toward this value.
TEXAS default is diffuse (10–15). Small = strong pull = big offset; large = the prior fades.
Scales the ensemble fan around the median curve. Wider → bigger mixture/nonlinearity gap.

Where the median gap comes from

Same proxy, built up in stages — relative to the plug-in point.

Source added|median gap|

What to actually do

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)
Sanity checks (try them in the sandbox)