Como calcular probabilidades condicionais no AnyDice?

8

Enquanto escrevia o adendo para esta resposta, que considera o valor relativo de habilidade versus característica no "sistema 3d20" de Neuroshima, Encontrei-me querendo uma resposta para uma pergunta enganosamente simples: quantos pontos de habilidade são necessários para ter sucesso se o teste mais baixo for um sucesso natural, versus se não for? Em outras palavras, eu basicamente queria traçar as distribuições de:

  • o rolo do meio de 3d20, dado que o rolo mais baixo é menor que um determinado limite x; E
  • a soma dos rolos mais baixos e médios, dado que o rolo mais baixo é pelo menos x.

Nas estatísticas, isso seria apenas um padrão distribuição de probabilidade condicional, por exemplo $$ p_x (y) = P (Y = y \ meio X <x), $$ $$ q_x (z) = P (X + Y = z \ meio X \ ge x), $$ onde \ $ X \ $ e \ $ Y \ $ são variáveis ​​aleatórias (interdependentes) que representam o rolo mais baixo e o meio do 3d20, respectivamente. Você pode calcular isso facilmente apenas tomando a distribuição conjunta de \ $ (X, Y) \ $, descartando os casos em que a condição (por exemplo, \ $ X <x \ $) falha, redimensionando as probabilidades restantes para que elas somarem 1 e, opcionalmente, somar sobre a variável de condicionamento \ $ X \ $ para obter a distribuição marginal de \ $ Y \ $ (Ou \ $ X + Y \ $).

Infelizmente, parece não haver uma maneira simples de fazer isso no AnyDice. De fato, nem parece haver nenhuma maneira de responder perguntas mais simples de probabilidade condicional como, digamos "qual é a soma média de 3d6 se a soma rolada for par versus vs. é ímpar?"

Então, daí esta pergunta: Existe alguma maneira de calcular uma distribuição de probabilidade condicional no AnyDice e, em caso afirmativo, como?


Isenção de responsabilidade: percebo que esta questão pode ser um tópico fora do tópico para este site, pois é mais uma questão de programação / matemática. Dito isso, ele surgiu em um contexto relacionado ao RPG - especificamente, ao escrever uma resposta aqui no RPG.SE - e eu suspeito que as respostas possam ser úteis para outras pessoas que usam o AnyDice para responder a perguntas semelhantes sobre outros sistemas. Vou deixar a comunidade decidir se essas perguntas e respostas devem permanecer aqui ou não.

Além disso, finalmente consegui encontrar uma solução (um pouco invasiva, mas viável) para o meu problema por conta própria, por isso postei uma resposta automática abaixo. Dito isto, outras respostas também são bem-vindas. Se existe uma maneira melhor de conseguir isso, eu gostaria muito de saber.

por Ilmari Karonen 21.04.2019 / 10:14

2 respostas

Use o resultado "dado vazio" para desconsiderar casos que não atendem aos critérios

Se queremos desconsiderar completamente um determinado subconjunto de resultados, podemos fazer isso usando uma função que retorna o "dado vazio", d {}, para casos que não atendem às condições desejadas.

O dado vazio d {} parece ser um dado especial que não tem resultados possíveis nem probabilidade associada. Conseqüentemente, se definirmos uma função que retorna esse dado vazio para certos casos de entrada, ele está removendo efetivamente esses casos do conjunto de resultados possíveis, e a distribuição de resultados que volta da função é como se os casos indesejados nunca fossem chamados.

Aqui está uma função simples que simplesmente limita a entrada recebida a um conjunto de valores permitidos e descarta casos que não atendem a essa condição:

função: se X: n em RESTRITO: s {se X = RESTRITO {resultado: X} resultado: d {}}

Dada uma entrada X, Se X pode ser encontrado na sequência de valores permitidos RESTRINGIRestá tudo bem e voltamos X; caso contrário, retornamos d {}, atribuindo probabilidade zero a esse resultado específico. Podemos usar esta função para restringir um rolo 3d6 a apenas valores pares ou ímpares:

saída [se 3d6 em {3,5,7,9,11,13,15,17}] denominada "3d6 se ímpar" saída [se 3d6 em {4,6,8,10,12,14,16,18}] denominada "3d6 se for par"

E obtemos um resultado parecido com o seguinte:

Qualquer gráfico de 3d6 restrito a ímpar ou par

Obviamente, isso se estende a casos mais interessantes, como as regras do Neuroshima fornecidas na pergunta. Aqui está um programa que mostra exemplos dessas distribuições:

function: INDEX:s at DICE:s if lowest less than MIN:n {
  if (#[email protected] >= MIN) { result: d{} }
  result: [email protected]
}

function: INDEX:s at DICE:s if lowest at least MIN:n {
  if (#[email protected] < MIN) { result: d{} }
  result: [email protected]
}

MIN: 10

output [2 at 3d20 if lowest less than MIN] named "Middle die of 3d20 if lowest die less than [MIN]"
output [{2,3} at 3d20 if lowest at least MIN] named "Middle and lowest die of 3d20 if lowest die at least [MIN]"

Essas funções primeiro descartam os casos que não atendem à condição especificada e depois nos fornecem os valores com os quais nos preocupamos nas seqüências de dados restantes.

É claro que você também pode abordar esse problema de outra maneira e definir funções que mapeiam resultados indesejados para um valor falso (como -1) e depois canalizar isso através de uma função de filtragem no final, que retira quaisquer resultados com o valor falso, embora fazer a filtragem o mais cedo possível é mais eficiente no Anydice e provavelmente permitirá que você execute programas mais complexos / conjuntos de dados maiores.

fundo

Eu bati nesse truque de dado vazio enquanto trabalhava esta resposta para outra pergunta. Essencialmente, eu escrevi uma função simples que re-rolaria recursivamente o 4d6-droplow até obter um 8 ou melhor, mas percebi na inspeção que a distribuição de resultados retornada não mudava, independentemente do que eu definisse a profundidade máxima da função.

Em Anydice, como diz a documentação, exceder a profundidade máxima da função simplesmente faz com que a função retorne o dado vazio e, a partir daí, descobri que isso significava que o dado vazio era essencialmente um resultado de probabilidade zero que não afeta a distribuição do resultado final, e que podemos devolvê-lo de propósito (em vez de acidentalmente exceder a profundidade da função) se quisermos desconsiderar alguma categoria de entradas!

18.09.2019 / 13:24

Acontece que lá is uma maneira de fazer isso no AnyDice, pelo menos mais ou menos. É um pouco hacky, mas funciona.

O segredo é relançar.

Especificamente, uma maneira geral de amostrar a partir de uma distribuição de probabilidade condicional é chamada amostragem por rejeição. Basicamente, você amostra um valor da distribuição de probabilidade original (não condicionada) e, se ela falhar na condição, você a rejeita e continua a reamostrar até obter um resultado que satisfaça a condição.

E podemos simular esse processo no AnyDice. Por exemplo, aqui está uma função AnyDice simples que pega um dado e o rotula novamente se seu valor não estiver em um determinado intervalo:

função: restringir ROLL: n para RANGE: s else REROLL: d {se ROLL = RANGE {resultado: ROLL} else {resultado: REROLL}} função: restringir ROLL: d para RANGE: s uma vez {resultado: [restringir ROLL para RANGE else ROLL]}

No entanto, isso apenas modela um rolar novamente, mas tudo bem. Podemos apenas iterar:

função: restringir ROLL: n para RANGE: s else REROLL: d {se ROLL = RANGE {resultado: ROLL} else {resultado: REROLL}} função: restringir ROLL: d para RANGE: s {loop I sobre {1..20 } {ROLL: [restringir ROLL a RANGE mais ROLL]} resultado: ROLL}

Agora, você pode olhar para este código e pensar que isso ainda faz os rerolls 20, mas esse não é o caso. Em vez disso, efetivamente faz o 220, ou cerca de um milhão de rerolls! O motivo dessa surpreendente eficiência é porque atualizamos o ROLO distribuição em cada iteração. Portanto, na segunda iteração, estamos amostrando a distribuição já relançada e, se a amostra for rejeitada, reamostrando a partir do mesmo distribuição já relançada. Então, basicamente, cada iteração dobra o número efetivo de rerolls.

Um milhão de rerolls não é bastante infinitamente, mas é bem próximo para a maioria dos propósitos. E se realmente não for suficiente (o que podemos identificar facilmente na saída, pelo fato de os valores supostamente rejeitados aparecerem com uma probabilidade diferente de zero), sempre podemos aumentar a contagem de iterações de 20 para, por exemplo, 30 para uma bilhão rerolls eficazes.

De qualquer forma, Aqui está um exemplo de como usar essa função:

saída [restringir 3d6 a {3,5,7,9,11,13,15,17}] denominada "3d6 se ímpar" saída [restringir 3d6 a {4,6,8,10,12,14,16,18}] denominada "3d6 se for par"

e um exemplo da saída:

Screenshot

(Surpreendentemente, acontece que a média é a mesma nos dois casos!)


Mas como podemos usar essa função para lidar com casos mais complexos, como o exemplo original de "meio do 3d20, se menor for menor que \ $ x \ $", onde a variável cuja distribuição queremos não é a mesma que queremos condicioná-la?

Bem, uma maneira bastante simples é escrever uma função que absorva o rolo de entrada (aqui, 3d20) como uma sequência e mapeie-a para a saída que queremos (por exemplo, o rolo do meio, neste caso) enquanto tb mapeando todos os casos rejeitados para algum resultado falso, como −1. Em seguida, podemos usar a função acima para rejeitar o resultado falso e obter a distribuição condicional que queremos, por exemplo, assim:

function: middle of ROLL:s if lowest in RANGE:s {
  if [email protected] = RANGE { result: [email protected] } \ assumes a three die pool! \
  else { result: -1 }
}

MAX: 10
DIST: [middle of 3d20 if lowest in {1..MAX}]

output DIST named "middle of 3d20 if lowest <= [MAX] (else -1)"
output [restrict DIST to {1..20}] named "middle of 3d20 if lowest <= [MAX] (conditional)"

O script que eu escrevi para a resposta original que inspirou essas perguntas e respostas é semelhante, embora faça uso de um truque adicional de redefinição de dados (descrito na resposta) para calcular facilmente o número de pontos de habilidade necessários para colocar as duas jogadas mais baixas abaixo do limite.

21.04.2019 / 10:14