Log-Logistic Survival Analysis

A tutorial on Bayesian survival analysis with the Log-Logistic distribution, supporting right-censored time-to-event data.

Introduction

The Log-Logistic distribution is popular in survival analysis because it:

This tutorial demonstrates using the loglogisticsurv family with censored data using the inla_surv response object.

The Model

For survival data, we observe $(t_i, \delta_i)$ for each subject $i$, where $t_i$ is the observed time and $\delta_i$ is the event indicator:

The survival function for variant 0 is:

$$ S_0(t) = 1 - F_0(t) = \frac{1}{1 + (\lambda t)^{\alpha}} $$

The hazard function is:

$$ h_0(t) = \frac{\alpha \lambda t^{\alpha-1}}{1 + \lambda t^{\alpha}} $$

The hazard is:

Dataset

The dataset contains $n = 1000$ observations with an event indicator:

ColumnDescriptionType
yObserved time (event or censoring)float
xCovariate (standardized)float
eventEvent indicator (1=event, 0=censored)int

Implementation in pyINLA

For survival models, use inla_surv to create the response object:

import pandas as pd
from pyinla import pyinla, inla_surv

# Load data
df = pd.read_csv('dataset_loglogistic_surv_v0.csv')

# Create survival response object
y_surv = inla_surv(
    time=df['y'].to_numpy(),
    event=df['event'].to_numpy()
)

# Define model with survival response
model = {
    'response': y_surv,
    'fixed': ['1', 'x']
}

# Specify variant in control
control = {
    'family': {
        'variant': 0
    },
    'compute': {
        'dic': True,
        'cpo': True,
        'mlik': True
    }
}

# Fit the survival model
result = pyinla(
    model=model,
    family='loglogisticsurv',  # Note: 'surv' suffix
    data=df,
    control=control
)

# View results
print(result.summary_fixed)
print(result.summary_hyperpar)

Expected Output

Fixed Effects

ParameterMeanSD2.5%97.5%
(Intercept)1.0400.0610.9211.160
x2.1340.0781.9822.288

Hyperparameters

ParameterMeanSD2.5%97.5%
alpha2.0620.0541.9572.169

Note: With all events observed (no censoring), the survival model gives identical results to the regression model.

Results and Diagnostics

The posterior summaries recover the true parameters well. The fitted median $\hat{\lambda}^{1/\alpha}$ provides a natural summary for the log-logistic survival model.

Observed survival times versus fitted median with identity line
Observed times $y_i$ versus fitted median $\hat{\lambda}_i^{1/\hat{\alpha}}$.
Residuals versus fitted median
Residuals $y_i - \hat{\lambda}_i^{1/\hat{\alpha}}$ versus fitted median.

To reproduce these figures locally, download the render_loglogisticsurv_plots.py script and run it alongside the CSV dataset.

Using Variant 1

# Load data generated with variant 1
df_v1 = pd.read_csv('dataset_loglogistic_surv_v1.csv')

# Create survival response
y_surv_v1 = inla_surv(
    time=df_v1['y'].to_numpy(),
    event=df_v1['event'].to_numpy()
)

model_v1 = {
    'response': y_surv_v1,
    'fixed': ['1', 'x']
}

control_v1 = {
    'family': {
        'variant': 1
    },
    'compute': {
        'dic': True
    }
}

result_v1 = pyinla(
    model=model_v1,
    family='loglogisticsurv',
    data=df_v1,
    control=control_v1
)

print(result_v1.summary_fixed)
print(result_v1.summary_hyperpar)

Censoring Types

The inla_surv object supports various censoring types:

# Example with mixed censoring
# For right-censored data: time is last observed, event=0
# For observed events: time is event time, event=1
# (`df['y']` holds the observed/censored time in the dataset used above.)

y_surv = inla_surv(
    time=df['y'].to_numpy(),
    event=df['event'].to_numpy()  # 1=event, 0=right-censored
)

Data Generation (Reference)

The survival dataset uses the same generation as the regression example, with an event indicator added:

import numpy as np
import pandas as pd

def rloglogistic(n, lam, alpha, variant=0, rng=None):
    """Generate random samples from a log-logistic distribution."""
    if rng is None:
        rng = np.random.default_rng()
    u = rng.uniform(size=n)
    if variant == 0:
        y = (lam / (1.0 / u - 1.0)) ** (1.0 / alpha)
    elif variant == 1:
        y = (1.0 / (1.0 / u - 1.0)) ** (1.0 / alpha) / lam
    return y

rng = np.random.default_rng(2026)
n = 1000
alpha = 2.1

# Generate covariate
x = rng.uniform(-1, 1, size=n)
x = (x - x.mean()) / x.std()

# Linear predictor
eta = 1.1 + 2.2 * x
lam = np.exp(eta)

# Generate log-logistic times
y = rloglogistic(n, lam=lam, alpha=alpha, variant=0, rng=rng)

# All events observed (no censoring in this example)
event = np.ones(n, dtype=int)

df = pd.DataFrame({
    'y': y,
    'x': np.round(x, 6),
    'event': event
})
# df.to_csv('dataset_loglogistic_surv_v0.csv', index=False)

Interpretation in AFT Framework

In the accelerated failure time (AFT) interpretation:

For example, with $\beta_1 = 2.134$: