¿Cómo muestrear una distribución multinomial truncada?

9

Necesito un algoritmo para probar una distribución multinomial truncada. Es decir,

x1Zp1x1pkxkx1!xk!

donde es una constante de normalización, tiene componentes positivos y . Solo considero los valores de en el rango .x k x i = n xZxkxi=nxaxb

¿Cómo puedo probar esta distribución multinomial truncada?

Nota: Consulte Wikipedia para obtener un algoritmo para muestrear una distribución multinomial no truncada. ¿Hay alguna manera de adaptar este algoritmo a una distribución truncada?

Versión uniforme: una versión más simple del problema es tomar todos los iguales, . Si puede diseñar un algoritmo para muestrear la distribución truncada en este caso al menos, publíquela. Aunque no es la respuesta general, eso me ayudaría a resolver otros problemas prácticos en este momento.p i = 1 / kpipi=1/k

llamar
fuente

Respuestas:

9

Si lo entiendo correctamente, desea muestrear los valores de la distribución multinomial con probabilidades modo que , sin embargo, desea que la distribución se trunca de modo que para todo .p 1 , ... , p k i x i = n a ix ib i x ix1,,xkp1,,pkixi=naixibixi

Veo tres soluciones (ni tan elegantes como en el caso no truncado):

  1. Aceptar rechazar. Muestra de multinomio no truncado, acepte la muestra si se ajusta a los límites de truncamiento; de lo contrario, rechace y repita el proceso. Es rápido, pero puede ser muy ineficiente.
rtrmnomReject <- function(R, n, p, a, b) {
  x <- t(rmultinom(R, n, p))
  x[apply(a <= x & x <= b, 1, all) & rowSums(x) == n, ]
}
  1. Simulación directa Muestra de manera que se asemeja al proceso de generación de datos, es decir, muestree canicas individuales de una urna aleatoria y repita este proceso hasta que muestree canicas en total, pero a medida que despliega el número total de canicas de una urna dada ( ya es igual a ) luego deje de dibujar de dicha urna. Implementé esto en un script a continuación.x i b inxibi
# single draw from truncated multinomial with a,b truncation points
rtrmnomDirect <- function(n, p, a, b) {
  k <- length(p)

  repeat {
    pp <- p         # reset pp
    x <- numeric(k) # reset x
    repeat {
      if (sum(x<b) == 1) { # if only a single category is left
        x[x<b] <- x[x<b] + n-sum(x) # fill this category with reminder
        break
      }
      i <- sample.int(k, 1, prob = pp) # sample x[i]
      x[i] <- x[i] + 1  
      if (x[i] == b[i]) pp[i] <- 0 # if x[i] is filled do
      # not sample from it
      if (sum(x) == n) break    # if we picked n, stop
    }
    if (all(x >= a)) break # if all x>=a sample is valid
    # otherwise reject
  }

  return(x)
}
  1. Algoritmo de metrópolis. Finalmente, el tercer enfoque más eficiente sería utilizar el algoritmo Metropolis . El algoritmo se inicializa mediante la simulación directa (pero se puede inicializar de manera diferente) para extraer la primera muestra . En los siguientes pasos de forma iterativa: el valor de propuesta se acepta como con probabilidad , de lo contrario se toma el valor es el lugar, donde. Como propuesta, utilicé la función que toma el valor y cambia aleatoriamente de 0 al número de casos y lo mueve a otra categoría.X1y=q(Xi1)Xif(y)/f(Xi1)Xi1f(x)ipixi/xi!qXi1step
# draw R values
# 'step' parameter defines magnitude of jumps
# for Meteropolis algorithm
# 'init' is a vector of values to start with
rtrmnomMetrop <- function(R, n, p, a, b,
                          step = 1,
                          init = rtrmnomDirect(n, p, a, b)) {

  k <- length(p)
  if (length(a)==1) a <- rep(a, k)
  if (length(b)==1) b <- rep(b, k)

  # approximate target log-density
  lp <- log(p)
  lf <- function(x) {
    if(any(x < a) || any(x > b) || sum(x) != n)
      return(-Inf)
    sum(lp*x - lfactorial(x))
  }

  step <- max(2, step+1)

  # proposal function
  q <- function(x) {
    idx <- sample.int(k, 2)
    u <- sample.int(step, 1)-1
    x[idx] <- x[idx] + c(-u, u)
    x
  }

  tmp <- init
  x <- matrix(nrow = R, ncol = k)
  ar <- 0

  for (i in 1:R) {
    proposal <- q(tmp)
    prob <- exp(lf(proposal) - lf(tmp))
    if (runif(1) < prob) {
      tmp <- proposal
      ar <- ar + 1
    }
    x[i,] <- tmp
  }

  structure(x, acceptance.rate = ar/R, step = step-1)
}

El algoritmo comienza en y luego deambula por las diferentes regiones de distribución. Obviamente, es más rápido que los anteriores, pero debe recordar que si lo usa para muestrear un pequeño número de casos, podría terminar con sorteos que están cerca uno del otro. Otro problema es que necesita decidir sobre el tamaño, es decir, qué tan grandes debe hacer el algoritmo: demasiado pequeño puede llevar a moverse lentamente, demasiado grande puede llevar a hacer demasiadas propuestas inválidas y rechazarlas. Puede ver un ejemplo de su uso a continuación. En los gráficos se pueden ver: densidades marginales en la primera fila, traceplots en la segunda fila y gráficos que muestran saltos posteriores para pares de variables.X1step

n <- 500
a <- 50
b <- 125
p <- c(1,5,2,4,3)/15
k <- length(p)
x <- rtrmnomMetrop(1e4, n, p, a, b, step = 15)

cmb <- combn(1:k, 2)

par.def <- par(mfrow=c(4,5), mar = c(2,2,2,2))
for (i in 1:k)
  hist(x[,i], main = paste0("X",i))
for (i in 1:k)
  plot(x[,i], main = paste0("X",i), type = "l", col = "lightblue")
for (i in 1:ncol(cmb))
  plot(jitter(x[,cmb[1,i]]), jitter(x[,cmb[2,i]]),
       type = "l", main = paste(paste0("X", cmb[,i]), collapse = ":"),
       col = "gray")
par(par.def)

ingrese la descripción de la imagen aquí

El problema con el muestreo de esta distribución es que describe una estrategia de muestreo muy ineficiente en general. Imagine que y , y 's están cerca de ' s, en tal caso desea muestrear categorías con diferentes probabilidades, pero espera que sean similares frecuencias al final. En casos extremos, imagine una distribución de dos categorías donde , y ,a 1 = = a k b 1 = b k a i b i p 1p 2 a 1a 2 b 1b 2p1pka1==akb1=bkaibip1p2a1a2b1b2, en tal caso, espera que suceda algo muy raro (un ejemplo real de dicha distribución sería un investigador que repita el muestreo hasta que encuentre la muestra que sea consistente con su hipótesis, por lo que tiene más que ver con el engaño que con el muestreo aleatorio) .

La distribución es mucho menos problemática si la define como Rukhin (2007, 2008) donde muestrea casos para cada categoría, es decir, muestra proporcionalmente a 's.p inpipi


Rukhin, AL (2007). Estadísticas de orden normal y sumas de variables geométricas aleatorias en problemas de asignación de tratamientos. Estadísticas y cartas de probabilidad, 77 (12), 1312-1321.

Rukhin, AL (2008). Reglas de detención en problemas de asignación equilibrada: distribuciones exactas y asintóticas. Análisis secuencial, 27 (3), 277-292.

Tim
fuente
Rechazar muestras no válidas puede ser demasiado lento. Probablemente sea más simple hacer una traducción, , . De esta manera solo tiene que preocuparse por el límite superior, . Luego, puede eliminar la línea donde rechaza la muestra si se viola la (se pueden concebir valores de donde este rechazo sería muy ineficiente) m = n - i a i y ib i - a i x a ayi=xiaim=niaiyibiaixaa
becko
@becko si compara ese enfoque con el descrito por mí, verá que ofrecen diferentes soluciones.
Tim
No entiendo cómo pueden ser diferentes? Todo lo que hice fue un cambio de variables.
Becko
@becko tu punto de partida es todo x[i] >= a. Imagine que arrojó una moneda sesgada con una probabilidad de cara = 0.9. Lanzas la moneda hasta que obtengas al menos 10 caras y 10 colas. En el punto de parada, tendrías en promedio muchas más caras que colas. Comenzar en x[1] = ... = x[k] = asignifica que ignora el hecho de que los puntos de partida de cada uno de ellos x[i]son diferentes debido a los diferentes p[i].
Tim
Te entiendo. Lo único que no me gusta de su solución es que creo que podría ser muy ineficiente para elecciones particulares de los parámetros.
Becko
1

Aquí está mi esfuerzo al tratar de traducir el código R de Tim a Python. Como pasé un tiempo entendiendo este problema y codifiqué los algoritmos en Python, pensé compartirlos aquí en caso de que la gente estuviera interesada.

  1. Algoritmo de aceptación-rechazo :
def sample_truncated_multinomial_accept_reject(k, pVec, a, b):
    x = list(np.random.multinomial(k, pVec, size=1)[0])
    h = [x[i] >= a[i] and x[i] <= b[i] for i in range(len(x))]
    while sum(h) < len(h):
        x = list(np.random.multinomial(k, pVec, size=1)[0])
        h = [x[i] >= a[i] and x[i] <= b[i] for i in range(len(x))]
    return x
  1. Simulación directa
def truncated_multinomial_direct_sampling_from_urn(k, pVec, a, b):
    n = len(pVec)
    while True:
        pp = pVec 
        x = [0 for _ in range(n)] 
        while True:
            if sum([x[h] < b[h] for h in range(n)])==1:
                indx = [h for h in range(n) if x[h] < b[h]][0]
                x[indx] = k - sum(x)
                break
            i = np.random.choice(n, 1, p=pp)[0]
            x[i] += 1
            if x[i] == b[i]:
                pp = [pp[j]/(1-pp[i]) for j in range(n)]
                pp[i] = 0 
            if sum(x) == k:
                break  
        if sum([x[h] < a[h] for h in range(n)]) == 0:
            break 
    return x 
  1. Algoritmo Metropolis
def compute_log_function(x, pVec, a, b):
    x_less_a = sum([x[i] < a[i] for i in range(len(pVec))])
    x_more_a = sum([x[i] > b[i] for i in range(len(pVec))])
    if x_less_a or x_more_a or sum(x) != k:
        return float("-inf")
    return np.sum(np.log(pVec)*x - np.array([math.lgamma(h+1) for h in x]))
def sampling_distribution(original, pVec, a, b, step):
    x = copy.deepcopy(original) 
    idx = np.random.choice(len(x), 2, replace=False)
    u = np.random.choice(step, 1)[0]
    x[idx[0]] -= u
    x[idx[1]] += u
    x_less_a = sum([x[i] < a[i] for i in range(len(pVec))])
    x_more_a = sum([x[i] > b[i] for i in range(len(pVec))])
    while x_less_a or x_more_a or sum(x) != k:
        x = copy.deepcopy(original)  
        idx = np.random.choice(len(x), 2, replace=False)
        u = np.random.choice(step, 1)[0]
        x[idx[0]] -= u
        x[idx[1]] += u
        x_less_a = sum([x[i] < a[i] for i in range(len(pVec))])
        x_more_a = sum([x[i] > b[i] for i in range(len(pVec))])
    return x 
def sample_truncated_multinomial_metropolis_hasting(k, pVec, a, b, iters, step=1):
    tmp=sample_truncated_multinomial_accept_reject(k, pVec, a, b)[0]
    step = max(2, step)
    for i in range(iters):
        proposal = sampling_distribution(tmp, pVec, a, b, step)
        if compute_log_function(proposal, pVec, a, b) == float("-inf"):
            continue             
        prob = np.exp(np.array(compute_log_function(proposal, pVec, a, b)) -\
                      np.array(compute_log_function(tmp, pVec, a, b)))
        if np.random.uniform() < prob:
            tmp = proposal 
        step -= 1 
    return tmp

Para una implementación completa de este código, consulte mi repositorio de Github en

https://github.com/mohsenkarimzadeh/sampling

Mohsen Kiskani
fuente