#8441·pymc

`LogitNormal` gets no default transform, unlike other unit-interval distributions

Author: AllenDowneyCreated Sep 15, 2026Updated Sep 15, 2026

Description

pm.LogitNormal is supported on the interval (0, 1), but unlike the other distributions on that support it is not assigned a default transform. As a result, NUTS operates on the constrained scale, proposes values outside the support, and a model using it fails at initialization.

Reproducible example

python
import pymc as pm

with pm.Model() as model:
    mu = pm.Normal("mu", 0, 2)
    sigma = pm.HalfNormal("sigma", sigma=1)
    xs = pm.LogitNormal("xs", mu=mu, sigma=sigma, shape=13)
    pm.Binomial("ks", n=[129] * 13, p=xs, observed=[4] * 13)
    idata = pm.sample()

Result:

pymc.exceptions.SamplingError: Initial evaluation of model at starting point failed!
Starting values:
{'mu': array(-0.58882144), 'sigma_log__': array(0.91378887), 'xs': array([ 0.19462499,  0.39542672,  1.05327918, -0.03184312,  0.82003634,
       -0.61738645,  1.26254703,  0.69715246,  0.60729211,  1.12854095,
        0.72072905, -0.44765787, -0.48220885])}
Logp initial evaluation results:
{'mu': np.float64(-1.66), 'sigma': np.float64(-2.42), 'xs': np.float64(-inf), 'ks': np.float64(-inf)}

Note that sigma appears as sigma_log__, because it did get its transform, while xs did not.

Note the negative entries in the starting value for xs. jitter+adapt_diag perturbs the initial point on the constrained scale, so with 13 components at least one lands outside (0, 1) essentially every time. I tried 6 seeds and it failed on all 6.

Comparison with other distributions on (0, 1)

python
for name, make in [
    ("Beta",        lambda: pm.Beta("v", 2, 2)),
    ("Kumaraswamy", lambda: pm.Kumaraswamy("v", 2, 2)),
    ("Uniform(0,1)", lambda: pm.Uniform("v", 0, 1)),
    ("LogitNormal", lambda: pm.LogitNormal("v", 0, 1)),
]:
    with pm.Model() as m:
        make()
    print(name, m.rvs_to_transforms[m.free_RVs[0]])
distribution support default transform base class
Beta (0, 1) LogOddsTransform UnitContinuous
Kumaraswamy (0, 1) LogOddsTransform UnitContinuous
Uniform(0, 1) (0, 1) Interval BoundedContinuous
LogitNormal (0, 1) none object

LogitNormal is the only one without a transform, and the only one that is not a subclass of a continuous distribution base class. It appears to be implemented as a wrapper that returns a CustomDist, which is presumably why it does not pick up the default transform that UnitContinuous provides.

For contrast, HalfNormal — also a constrained distribution — does get its LogTransform automatically, so the inconsistency is specific to this distribution rather than general.

Workaround

python
from pymc.distributions.transforms import logodds

xs = pm.LogitNormal("xs", mu=mu, sigma=sigma, shape=13,
                    default_transform=logodds)

With this, the model initializes and samples normally.

(For the hierarchical model above, a non-centered parameterization — xs = invlogit(mu + sigma * us) with us ~ Normal(0, 1) — both avoids the issue and samples better, but that is a modeling choice and does not address the inconsistency in the distribution itself.)

Question

Should LogitNormal be implemented as a proper UnitContinuous distribution, or at minimum default to the logodds transform? As it stands the failure is hard to diagnose: the error points at the starting values rather than at the missing transform, and nothing in the distribution's documentation suggests it behaves differently from Beta.

Happy to open a PR if there is a preferred approach.

Versions

  • PyMC 6.3.1
  • PyTensor 3.3.0
  • Python 3.12.13
  • Linux