Introduction to likelihood.contr

The Problem

Suppose you observe failure times from a Weibull distribution, but some units are right-censored: you only know they survived past a cutoff. Exact observations contribute log f(t) to the log-likelihood while censored ones contribute log S(t). Both share the same shape and scale parameters, but the math is different for each row.

likelihood.contr handles this by letting you declare each observation type’s contribution separately, then composing them into a single model.

Declaring Contributions

contr_name() generates a contribution from any R distribution with d<name> and p<name> functions. Four censoring types are supported:

Type Log-likelihood contribution
"exact" log f(x; theta) via d<name>
"right" log S(x; theta) via p<name>(lower.tail=FALSE)
"left" log F(x; theta) via p<name>
"interval" log [F(b; theta) - F(a; theta)] via p<name>
library(likelihood.contr)
library(likelihood.model)

exact <- contr_name("weibull", "exact", ob_col = "t")
right <- contr_name("weibull", "right", ob_col = "t")

Composing a Model

likelihood_contr() combines contributions and specifies how to dispatch rows to the right contribution. Here obs_type = "status" means the status column determines which contribution handles each row:

model <- likelihood_contr(
  obs_type = "status",
  exact = exact,
  right = right
)
model
#> Likelihood Contribution Model
#> -----------------------------
#> Observation types: exact, right 
#> Dispatch method: column 'status'
#> Assumptions:
#>  - iid

The contribution names (exact, right) must match the values in the dispatch column.

Simulating Censored Data

set.seed(42)
true_shape <- 2
true_scale <- 5
censor_time <- 4

raw_times <- rweibull(300, shape = true_shape, scale = true_scale)
df <- data.frame(
  t      = pmin(raw_times, censor_time),
  status = ifelse(raw_times <= censor_time, "exact", "right")
)
table(df$status)
#> 
#> exact right 
#>   142   158

Fitting the Model

The fit() generic returns a solver function. Call it with data and starting values:

result <- suppressWarnings(
  fit(model)(df, par = c(shape = 1.5, scale = 4))
)
summary(result)
#> Maximum Likelihood Estimate (Fisherian)
#> ----------------------------------------
#> 
#> Coefficients:
#>       Estimate Std. Error   2.5% 97.5%
#> shape   2.0672     0.1611 1.7514 2.383
#> scale   4.9571     0.2309 4.5046 5.410
#> 
#> Log-likelihood: -383.9 
#> AIC: 771.8 
#> Number of observations: 300

The result is a fisher_mle object with the usual methods:

coef(result)
#>    shape    scale 
#> 2.067153 4.957124
confint(result)
#>           2.5%    97.5%
#> shape 1.751370 2.382936
#> scale 4.504556 5.409692

Evaluating the Log-Likelihood Directly

Every likelihood.model generic returns a closure. This is useful for profiling or plotting:

ll_fn <- loglik(model)

# Evaluate at two different parameter vectors
ll_fn(df, par = c(shape = 2, scale = 5))
#> [1] -383.973
ll_fn(df, par = c(shape = 1, scale = 3))
#> [1] -485.7462

Function-Based Dispatch

When the observation type depends on multiple columns or a computed condition, pass a function instead of a column name:

model_fn <- likelihood_contr(
  obs_type = function(df) ifelse(df$delta == 1, "exact", "right"),
  exact = contr_name("exp", "exact", ob_col = "t"),
  right = contr_name("exp", "right", ob_col = "t")
)

df_fn <- data.frame(t = c(0.5, 1.0, 2.0), delta = c(1, 0, 1))
loglik(model_fn)(df_fn, par = c(rate = 1.5))
#> [1] -4.43907

Next Steps

For user-defined log-likelihood functions, analytical derivatives, and model comparison via likelihood ratio tests, see vignette("custom-contributions").

mirror server hosted at Truenetwork, Russian Federation.