DEV Community

El acertijo de Monty Hall

El acertijo o Problema de Monty Hall ya es ampliamente conocido a estas alturas. De hecho, incluso el problema de Monty Hall tiene su propia entrada en la Wikipedia. Pero no lo mires todavía, ¡vas a estropear la diversión! Más aún, si le planteas este problema a una IA, lo reconocerá sin dudar aunque no le indiques cuál es su nombre.

El acertijo de Monty Hall con sus tres puertas.

Recientemente, Raquel de la Morena y su equipo han dedicado un vídeo a explicar el problema de Monty Hall. ¿He dicho ya que si te lo ves ahora, vas a estropear la diversión?

Todo proviene de un concurso presentado por Monty Hall (de ahí el nombre por el que es conocido). En este concurso se presentaban tres puertas, detrás de las cuales había dos cabras y un coche deportivo. El concurso consistía, claro, en acertar con la puerta que escondía el coche. Es posible que a alguno le gustase la posibilidad de llevarse a una cabra a casa, pero no tengo ni idea de si era posible o no. El caso es que, en este concurso, el presentador podía abrir una de las puertas donde él sabía que no estaba el coche. En estos casos, ¿era más ventajoso mantener la elección inicial de la puerta, o en cambio, elegir la que aún está cerrada?

El concurso de Monty Hall, hay dos cabras detrás de dos de las tres puertas.

Una mujer, reconocida como poseedora del índice de inteligencia mayor del mundo, Marilyn vos Savant, tenía una columna en un periódico para responder preguntas (se supone que especialmente difíciles, claro). Un lector le propuso la misma pregunta un poco más arriba. Su respuesta fue polémica: es mucho más recomendable cambiar de puerta.

Si pensaste "da igual", estás, como yo, entre la gran mayoría de personas. Si pensaste lo mismo que Marilyn vos Savant, entonces estás entre los "escogidos", la gente excepcionalmente inteligente que no se deja engañar por las apariencias. Porque este problema se plantea como una paradoja, es decir, casi todo el mundo, independientemente de sus conocimientos o formación, se decanta por la primera opción, la mayoritaria. Mayoritaria e incorrecta.

De hecho, buena parte de la polémica que se lió con aquella columna tomó la forma de miles de cartas, alrededor de diez mil. Más aún, de estas, mil provenían de matemáticos que estaban visceralmente en desacuerdo con esta mujer, lo que dejaron claro con una actitud claramente clasista, y también machista.

¿Por qué la respuesta es la que no es intuitiva? Eso es lo que resolveremos mediante código Python, con el que simularemos, utilizando números pseudo-aleatorios (lo que vendría a ser un método Montecarlo), varias jugadas, a ver cuál estrategia (cambiar o no cambiar), es la que objetivamente resulta ser la mejor.


import random as rnd


def generate_random_list(max):
    toret = []
    for _ in range(max):
        toret.append(rnd.randint(1, 3))
    ...

    return toret
...


if __name__ == "__main__":
    print(str.join(", ", [str(x) for x in generate_random_list(10)]))
    print(str.join(", ", [str(x) for x in generate_random_list(10)]))
...

Enter fullscreen mode Exit fullscreen mode

Esto puede producir la siguiente salida:

3, 2, 2, 1, 3, 3, 3, 1, 2, 1
2, 1, 2, 2, 2, 3, 1, 2, 3, 2
Enter fullscreen mode Exit fullscreen mode

Podríamos interpretarla de la manera siguiente: la fila superior es el número de puerta (de 1 a 3) en la que se sitúa el premio, mientras que la segunda fila es el número de puerta (también de 1 a 3) elegida por el concursante. Podemos maquillar un poco la salida:

0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 0
-------------------------------------
1 | 1 | 3 | 2 | 3 | 1 | 1 | 3 | 1 | 3
3 | 1 | 3 | 2 | 2 | 1 | 2 | 2 | 3 | 1
-------------------------------------
Aciertos:  4
Enter fullscreen mode Exit fullscreen mode

Esto lo obtenemos cambiando un poco el código, como se ve a continuación (no se incluye la función generate_random_list(), que se mantiene tal cual).

def count_coincidences(l1, l2):
    toret = 0
    for i in range(len(l1)):
        if l1[ i ] == l2[ i ]:
            toret += 1
        ...
    ...

    return toret
...


if __name__ == "__main__":
    rnd.seed()
    l_elecciones = generate_random_list(10)
    l_premios = generate_random_list(10)
    print("0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1")
    print("1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 0")
    print("-------------------------------------")
    print(str.join(" | ", [str(x) for x in l_elecciones]))
    print(str.join(" | ", [str(x) for x in l_premios]))
    print("-------------------------------------")
    print("Aciertos: ", count_coincidences(l_elecciones, l_premios))    
...
Enter fullscreen mode Exit fullscreen mode

Lo que estamos haciendo es utilizar listas paralelas. En la primera, colocamos las elecciones realizadas, mientras en la segunda colocamos las puertas con premios. Podemos compactarlo todo un poco si utilizamos una sola lista, pero de pares. Cada par está formado por la misma información. Es decir, si la puerta elegida fue la primera, y la puerta con premio la tercera, el par será (1, 3). Si hacemos una prueba con 10 puertas, como más arriba, tendremos una lista como [(1, 3, (1, 1), (3, 3), (2, 2), (3, 2), (1, 1), (1, 2), (3, 2), (1, 3), (3, 1)], mientras antes teníamos dos listas: [1, 1, 3, 2, 3, 1, 1, 3, 1, 3], y [3, 1, 3, 2, 2, 1, 2, 2, 3, 1].

def generate_random_list_door_pairs(max, num_doors=3):
    toret = []
    for _ in range(max):
        toret.append(
                tuple([rnd.randint(1, num_doors),
                        rnd.randint(1, num_doors)]))
    ...

    return toret
...
Enter fullscreen mode Exit fullscreen mode

Además, podemos mover el código para visualizar los datos a su propia función. Ahora tenemos que tener en cuenta que recibimos una sola lista, de pares.

def print_to(f, l_pairs):
    print("0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1", file=f)
    print("1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 0", file=f)
    print("-------------------------------------")
    print(
            str.join(" | ", [str(x) for x in
                                [pair[0] for pair in l_pairs]]),
            file=f)
    print(
            str.join(" | ", [str(x) for x in
                                [pair[1] for pair in l_pairs]]),
            file=f)
    print("-------------------------------------", file=f)
    num_correct = len(extract_coincidences(l_pairs))
    total = len(l_pairs)
    percentage = (num_correct / total) * 100
    print(f"Correct: {num_correct}/{total} ({percentage:5.2f}%)\n", file=f)
...
Enter fullscreen mode Exit fullscreen mode

Finalmente, podemos extraer los pares con ambos valores iguales, en lugar de contarlos directamente. Tomando la longitud de esta lista de pares extraidos, tenemos el número de coincidencias. En lugar de utilizar un bucle, utilizaremos una comprensión de listas.

def extract_coincidences(l_pairs):
    return [pair for pair in l_pairs if pair[0] == pair[1]]
...
Enter fullscreen mode Exit fullscreen mode

Podemos invocar el código con:

if __name__ == "__main__":
    # more...
    max = 100000

    if args.get("verbose"):
        max = 10
    ...

    generated_pairs = generate_random_list_door_pairs(max)

    if max < 40:
        print_to(sys.stdout, generated_pairs)
    ...
...
Enter fullscreen mode Exit fullscreen mode

La salida sería como la siguiente:

0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 0
-------------------------------------
3 | 1 | 3 | 1 | 3 | 3 | 2 | 1 | 3 | 2
1 | 3 | 1 | 3 | 1 | 1 | 2 | 3 | 2 | 2
-------------------------------------
Correct: 2/10 (20.00%)
Enter fullscreen mode Exit fullscreen mode

Claramente vemos que hay dos coincidencias, en la 7ª y 10ª (esto depende de la ejecución, en otras podemos llegar incluso al 50%). Podemos utilizar matplotlib para crear una gráfica, un simple gráfico de barras sobre el porcentaje de aciertos.

def show_graph(percentage_correct, msg_explain):
    plt.bar(
        [msg_explain],
        [percentage_correct],
        color=["blue", "green"])

    plt.yticks(range(0, 101, 10))
    plt.title("Monty Hall paradox")
    plt.ylabel("Percentage (0-100%)")
    plt.show()
...
Enter fullscreen mode Exit fullscreen mode

Si invocamos todo esto con un número considerable de pruebas, es decir, si la lista de pares es de, pongamos, 10000, conseguiremos una estadística suficientemente fiable, respaldada por un número de casos considerable.

if __name__ == "__main__":
    # more...
    max = 100000

    if args.get("verbose"):
        max = 10
    ...

    generated_pairs = generate_random_list_door_pairs(max)
    msg = "correct selections (no door change)"
    correct_pairs = extract_coincidences(generated_pairs)
    num_correct = len(correct_pairs)
    percentage = (num_correct / max) * 100

    if max < 40:
        print_to(sys.stdout, generated_pairs)
    ...

    show_graph(percentage, f"{msg}: {percentage: 5.2f}%")
...
Enter fullscreen mode Exit fullscreen mode

La gráfica sería la siguiente:

Estrategia sin cambio de puerta, con 100,000 casos generados.

Podemos ver cómo los casos con acierto de premio alcanzan un 33.2% del total.

Esto tiene todo el sentido. Si no cambiamos de puerta, lo que estamos haciendo es confiar en que nuestra primera elección fue desde el principio la correcta, que esa es la puerta con premio. Dado que hay tres puertas, la probabilidad es de 1/3 o 33% sobre el total.

Podemos ahora abordar la estrategia de cambiar de puerta. Para crear un par (puerta_con_premio, puerta_elegida), si cambiamos de puerta tras revelar otra (sin premio), podemos utilizar el siguiente código:

def create_selection_changed(num_doors=3):
    # Initial choice
    doors = list(range(1, num_doors + 1))
    treasure_door = rnd.choice(doors)
    chosen_door = rnd.choice(doors)

    # One door not containing the treasure is revealed
    # Also it won't be the selected door
    door_revealed = set(doors)
    door_revealed.discard(treasure_door)
    door_revealed.discard(chosen_door)
    revealed_door = rnd.choice(list(door_revealed))

    # Change your chosen door
    available_doors = set(doors)
    available_doors.discard(chosen_door)
    available_doors.discard(revealed_door)
    chosen_door2 = rnd.choice(list(available_doors))

    return tuple([treasure_door, chosen_door2])
...
Enter fullscreen mode Exit fullscreen mode

La puerta elegida es chosen_door, mientras que la prueba tras la que se esconde el premio es la treasure_door. Creamos un conjunto set() con las tres puertas (es decir, {1, 2, 3}), llamado door_revealed. Si a este conjunto le quitamos la puerta premiada (nunca revelaremos la puerta premiada), y también la puerta elegida por el concursante (estas dos puertas pueden ser la misma), entonces nos quedan las puertas que pueden ser reveladas (en el caso de tres puertas, serán dos posibles puertas si el concursante ha elegido la puerta premiada, o una sola en otro caso).

Así, empezamos con {1, 2, 3}, y tenemos en cuenta que la puerta premiada es la 1, y la elegida por el concursante es la 3. Así, la puerta a revelar es la 2. El concursante tiene la duda de mantenerse con la que eligió inicialmente (la 1), o cambiar (a otra que no sea ni la 1, ni la 2, una la elegida inicialmente, y la otra la revelada como que no tiene premio). Es decir, si tenemos {1, 2, 3}, eliminamos la 1 y la 2, y nos queda que la puerta elegida es la 3. Así, finalmente el par generado sería (3, 3).

Si empezamos igual que antes, con {1, 2, 3} pero con la puerta premiada siendo la 3, y la puerta elegida siendo también la 3, entonces la puerta a revelar puede ser la 1 o la 2, lo cual se elige al azar. Para cambiar la puerta, eliminamos la puerta elegida inicialmente, la 3, y la puerta revelada (pongamos que la 1), entonces nos queda que la única posibilidad para poder cambiar es la 2. Por tanto, el par ahora sería (3, 2).

Podemos crear fácilmente el número que deseemos de estos pares con la estrategia de cambio de puerta mediante la siguiente función:

def generate_random_list_door_pairs_after_change(max, num_doors=3):
    selections_changed = []

    for _ in range(max):
        selections_changed.append(create_selection_changed(num_doors))
    ...

    return selections_changed
...
Enter fullscreen mode Exit fullscreen mode

Solo tenemos que invocar el código de la forma siguiente:

if __name__ == "__main__":
    # more...
    max = 100000

    if args.get("verbose"):
        max = 10
    ...

    generated_pairs = generate_random_list_door_pairs_after_change(max)
    msg = "correct selections (door changed)"
    correct_pairs = extract_coincidences(generated_pairs)
    num_correct = len(correct_pairs)
    percentage = (num_correct / max) * 100

    if max < 40:
        print_to(sys.stdout, generated_pairs)
    ...

    show_graph(percentage, f"{msg}: {percentage: 5.2f}%")
...
Enter fullscreen mode Exit fullscreen mode

Y ya está. El código Python completo para esta paradoja de Monty Hall es un poco distinto porque permite seleccionar el cálculo de la estrategia con cambio, la estrategia sin cambio, o ambas para poder compararlas.

Si creamos, de nuevo, una lista de casos de 100000 elementos, tendremos una base de casos suficiente como para respaldar una estadística fiable.

Si lo invocamos, la salida será la siguiente.

Estrategia con cambio de puerta, con 100,000 casos generados.

Ahora, lo que tenemos es una probabilidad de acierto... ¡de más del 66%!

El software creado nos permite probar ambas estrategias, y compararlas en una gráfica común, como se puede ver a continuación.

Ambas estrategias, comparadas con 100,000 casos.

Pues resulta que hemos demostrado que, efectivamente, la estrategia que proporciona más probabilidad de acierto es el cambio de puerta. La clave resulta ser que la puerta revelada no lo es al azar, sino que el presentador la escoge entre las puertas que no tienen premio y que el concursante no ha elegido, lo que al final en lugar de 1/3 tenemos 2/3 de posibilidades de acertar. Es decir, más de un 66%.

Supongamos que en lugar de tres, son diez puertas. El concursante escoge una de las puertas, con 1/10 de probabilidades de acertar, y por tanto, 9/10 de errar. El presentador le revela una puerta donde puede estar el premio o no, además de la elegida inicialmente. Y además, le dice que el resto de las puertas no tienen premios. Es decir, de 10 puertas el concursante escoge la 3. El presentador le revela que en la 9 puede haber o no premio, pero que en todas las restantes (1, 2, 4, 5, 6, 7, 8, 10), solo hay cabras.

¿Es más probable que hayamos acertado en nuestra elección inicial (1/9 de posibilidades de acertar con la puerta del premio), o será mejor cambiar a esa puerta que estaba inicialmente entre el 9/10 restante?

Código Python completo para simular la paradoja de Monty Hall

Top comments (0)