BUGS Implementation Tricks

Implementing Custom Distributions Without Low-Level Coding in Previous Version of BUGS

In JuliaBUGS, users can simply create new distributions using the Distributions.jl interface and use them as built-in distributions. In previous versions of BUGS, defining new distributions required lower-level programming, as users needed to work directly with the underlying implementation.

Here we present some of the tricks that were used in previous BUGS implementations to create custom distributions without implementing them directly at the low level. These approaches are still valid in JuliaBUGS, though the native distribution interface is generally preferred.

The "Zeros Trick"

When you need a distribution not included in the standard set, the "zeros trick" offers an elegant solution. This technique leverages the fact that a Poisson observation with mean $\phi$ and value 0 has likelihood $e^{-\phi}$.

How it works:

  1. Create artificial data points of zeros
  2. Set $\phi[i] = -\log(L[i]) + C$, where:
    • \[L[i]\]

      is your desired likelihood term
    • \[C\]

      is a constant large enough to ensure $\phi[i] > 0$

Implementation:

C <- 10000    # Large constant ensuring phi[i] > 0

for (i in 1:N) {
    zeros[i] <- 0
    phi[i] <- -log(L[i]) + C
    zeros[i] ~ dpois(phi[i])
}

This method is particularly useful for implementing truncated distributions or any arbitrary likelihood function.

The "Ones Trick"

An alternative approach uses Bernoulli observations fixed at 1:

How it works:

  1. Create artificial data points of ones
  2. Define probabilities proportional to your desired likelihood: $p[i] = \frac{L[i]}{C}$
  3. Choose $C$ large enough to ensure $p[i] < 1$

Implementation:

C <- 10000    # Large constant ensuring p[i] < 1

for (i in 1:N) {
    ones[i] <- 1
    p[i] <- L[i] / C
    ones[i] ~ dbern(p[i])
}

Using dloglik in OpenBUGS and MultiBUGS

The dloglik distribution provides a more direct approach for implementing custom likelihoods:

dummy[i] <- 0
dummy[i] ~ dloglik(logLike[i])

Where logLike[i] is the log-likelihood contribution for observation $i$. This essentially implements the "zeros trick" behind the scenes.

Example: Manual Normal Likelihood Implementation

model {
   for (i in 1:7) {
      dummy[i] <- 0
      dummy[i] ~ dloglik(logLike[i])
      logLike[i] <- -log(sigma) - 0.5 * pow((x[i] - mu)/sigma, 2)         
   }
   mu ~ dunif(-10, 10)
   sigma ~ dunif(0, 10)
}

Standard equivalent:

model {
   for (i in 1:7) {
      x[i] ~ dnorm(mu, prec)
   }
   prec <- 1 / (sigma * sigma)
   mu ~ dunif(-10, 10)
   sigma ~ dunif(0, 10)
}

Implementing Custom Prior Distributions

You can use dloglik to implement non-standard prior distributions:

theta ~ dflat()           # Use flat improper prior as base
dummy <- 0
dummy ~ dloglik(logLike)  # Add custom prior via log-likelihood
logLike <- log(desired_prior_for_theta)

Example: Manual Normal Prior Implementation

model {
   for (i in 1:7) {
      x[i] ~ dnorm(mu, prec)
   }
   dummy <- 0
   dummy ~ dloglik(phi)
   phi <- -0.5 * pow(mu, 2)  # log(N(0,1))
   mu ~ dflat()              # Base distribution
   prec <- 1 / (sigma * sigma)
   sigma ~ dunif(0, 10)
}

Standard equivalent:

model {
   for (i in 1:7) {
      x[i] ~ dnorm(mu, prec)
   }
   mu ~ dnorm(0, 1)
   prec <- 1 / (sigma * sigma)
   sigma ~ dunif(0, 10)
}

Note: Using dloglik for priors may trigger Metropolis sampling, potentially leading to slower convergence and higher Monte Carlo errors.

Working with Predictions and Complex Models

Predicting New Observations

To generate predictions for a new observation x.pred, specify it as missing and assign an improper uniform prior:

x.pred ~ dflat()  # Improper uniform prior

Be aware this approach may increase computational inefficiency and Monte Carlo error.

Handling Model Mixtures of Different Complexity

For mixture models with components of varying complexity, a standard mixture distribution approach is often sufficient without requiring reversible jump techniques:

model {
   mu ~ dunif(-5, 5)
   p ~ dunif(0, 1)
   m[1] <- 0       # First component mean
   m[2] <- mu      # Second component mean
   
   for (i in 1:100) {
      group[i] ~ dbern(p)           # Component membership
      index[i] <- group[i] + 1
      y[i] ~ dnorm(m[index[i]], 1)  # Observation from selected component
   }
}

Managing Random Set Sizes

When loop bounds depend on random quantities (e.g., changepoints), use step functions to conditionally include elements:

for (i in 1:N) {
   ind[i] <- 1 + step(i - K - 0.01)  # 1 if i ≤ K, 2 if i > K
   y[i] ~ model[ind[i]]              # Select appropriate model
}

Example: Computing the Sum of First K Integers

model {
   # Define possible values for K
   for (i in 1:10) {
      p[i] <- 1/10  # Equal probability for each value
      x[i] <- i     # Value i
   }
   
   # Random selection of K
   K ~ dcat(p[])
   
   # Sum elements conditionally
   for (i in 1:10) {
      xtosum[i] <- x[i] * step(K - i + 0.01)  # Include x[i] only if i ≤ K
   }
   
   # Compute final sum
   s <- sum(xtosum[])
}