"확률? 그거 그냥 상식 아니야?" 우리는 흔히 이렇게 생각합니다. 동전을 던지면 반반이고, 주사위를 던지면 6분의 1. 일상생활에서 이 정도 직관이면 충분해 보입니다. 하지만 통계학자가 "직관을 믿지 마세요"라고 말하는 데는 이유가 있습니다.
우리의 뇌는 수만 년 동안 사자에게 쫓기며 생존하는 데 최적화되었지, 복잡한 확률을 계산하도록 진화하지 않았기 때문입니다. 오늘은 전 세계의 수학 박사들조차 멘붕에 빠뜨렸던 유명한 퀴즈 쇼 이야기로 시작해보려 합니다.
여러분이 TV 퀴즈 쇼의 최종 단계에 올라갔다고 상상해 보세요. 눈앞에는 굳게 닫힌 문 3개가 있습니다.
문 뒤에는 최신형 스포츠카 1대와, 꽝인 염소 2마리가 숨겨져 있습니다.
여러분은 1번 문을 선택했습니다. (두근두근)
이때, 정답을 알고 있는 사회자(몬티홀)가 묘한 미소를 지으며 여러분이 고르지 않은 문 중 하나를 엽니다. 3번 문이 열리고, 그 안에는 '메~' 하고 우는 염소가 있습니다.
이제 남은 문은 1번(내 선택)과 2번뿐입니다. 사회자가 제안합니다. "지금 선택을 바꾸시겠습니까? 2번 문으로 가시겠어요, 아니면 1번에 남으시겠습니까?"
자, 여러분이라면 어떻게 하시겠습니까?
대부분의 사람(심지어 수학자들도)은 이렇게 생각합니다.
"염소 하나가 사라졌으니, 남은 문은 두 개잖아. 그럼 확률은 반반(50%)이지. 굳이 바꿀 필요가 있어? 나는 내 처음 촉을 믿겠어."
이것은 우리의 직관이 속삭이는 아주 달콤한 거짓말입니다. 결론부터 말씀드리면, 여러분은 무조건 바꿔야 합니다. 선택을 바꾸면 당첨 확률이 2배나 껑충 뛰거든요.
"에이, 말도 안 돼."라는 생각이 드시죠? 실제로 1990년 미국에서 이 문제가 신문에 실렸을 때, 1만 명의 독자가 "수학 공부 다시 해라"라며 항의 편지를 보냈다고 합니다. 인간의 직관으로는 도저히 납득이 안 되기 때문입니다.
그래서 우리는 말싸움 대신, 파이썬(Python)이라는 심판을 불러올 겁니다. 컴퓨터는 거짓말을 하지 않으니까요.
우리가 직접 1만 번 게임을 할 수는 없으니, 파이썬에게 시켜봅시다. 코드는 아주 간단합니다.
바꾸지 않는 전략 (Stay)
바꾸는 전략 (Switch)
이 두 가지 경우를 각각 10,000번씩 시뮬레이션해서 진짜 승률이 50:50인지 확인해 보는 것입니다.
먼저 코드를 몬티홀 시뮬레이션 코드를 초보자분들도 이해하기 쉽게 설명해 드릴게요.
이 코드는 유명한 확률 문제인 '몬티홀 문제'를 파이썬으로 시뮬레이션하여, 어떤 전략(처음 선택을 고수할 것인가, 아니면 선택을 바꿀 것인가)이 더 유리한지 보여줍니다.
각 부분을 자세히 살펴볼까요?
1. 필요한 라이브러리 가져오기
import random
import random: 파이썬의 random 모듈을 가져옵니다. 이 모듈은 무작위 숫자를 생성하거나 무작위로 선택할 때 사용됩니다. 몬티홀 시뮬레이션에서는 자동차의 위치, 참가자의 첫 선택, 사회자가 열 문 등을 무작위로 결정하는 데 사용됩니다.
2. 몬티홀 시뮬레이션 함수 정의 (monty_hall_simulation)
def monty_hall_simulation(n_games, strategy):
wins = 0
doors = [0, 1, 2] # 문 3개 (0번, 1번, 2번)
def monty_hall_simulation(n_games, strategy):: monty_hall_simulation이라는 이름의 함수를 정의합니다. 이 함수는 두 가지 정보를 입력받습니다. n_games: 시뮬레이션을 몇 번 반복할지 (예: 10,000번). strategy: 참가자가 어떤 전략을 사용할지 ('stay'는 선택을 유지, 'switch'는 선택을 변경).
wins = 0: 이 변수는 참가자가 게임에서 이긴 횟수를 기록합니다. 초기에는 0으로 설정됩니다.
doors = [0, 1, 2]: 세 개의 문을 나타내는 리스트입니다. 각 문은 0, 1, 2번으로 지칭됩니다.
3. 게임 반복 (for 루프)
for _ in range(n_games):
# 1. 게임 세팅
car = random.choice(doors) # 자동차 위치 랜덤 설정
choice = random.choice(doors) # 참가자의 첫 선택
# 2. 사회자가 염소 문을 하나 열어줌 (정답 아니고, 참가자 선택도 아닌 문)
# (리스트 컴프리헨션: 남은 문 중에서 고름)
goat_doors = [d for d in doors if d!= car and d!= choice]
monty_open = random.choice(goat_doors)
# 3. 전략에 따른 최종 선택
if strategy == 'switch':
# 사회자가 연 문과 내 처음 선택을 제외한 나머지 문으로 변경
final_choice = [d for d in doors if d!= choice and d!= monty_open][0]
else: # 'stay'
final_choice = choice
# 4. 결과 확인
if final_choice == car:
wins += 1
for _ in range(n_games):: n_games 횟수만큼 게임을 반복합니다. _는 반복 횟수 자체가 중요하지 않을 때 사용하는 관례적인 변수명입니다.
게임 세팅 car = random.choice(doors): 세 문 중 하나를 무작위로 골라 자동차가 있는 문으로 정합니다. choice = random.choice(doors): 참가자가 세 문 중 하나를 무작위로 선택합니다. 이것이 참가자의 첫 번째 선택입니다.
사회자가 염소 문을 열어줌 goat_doors = [d for d in doors if d!= car and d!= choice]: '염소 문' 리스트를 만듭니다. 사회자는 자동차가 없는 문이면서 동시에 참가자가 처음 선택하지 않은 문을 열어야 합니다. 만약 참가자가 자동차가 있는 문을 골랐다면, 염소 문은 두 개가 남습니다. 사회자는 그중 아무거나 하나를 엽니다. 만약 참가자가 염소 문을 골랐다면, 자동차 문과 다른 염소 문 하나가 남습니다. 사회자는 반드시 그 다른 염소 문을 열어야 합니다. (이때 goat_doors 리스트에는 염소 문 하나만 남게 됩니다.) monty_open = random.choice(goat_doors): 사회자가 goat_doors 리스트에 있는 문 중 하나를 무작위로 선택하여 엽니다.
전략에 따른 최종 선택 if strategy == 'switch': 만약 참가자의 전략이 '바꾸기'라면, final_choice = [d for d in doors if d!= choice and d!= monty_open][0]: 사회자가 연 문(monty_open)과 참가자의 첫 선택(choice)을 제외한 나머지 문을 최종 선택으로 합니다. 이 경우 남는 문은 항상 하나입니다. else: # 'stay': 만약 참가자의 전략이 '유지하기'라면, final_choice = choice: 처음 선택했던 문을 그대로 최종 선택으로 합니다.
결과 확인 if final_choice == car:: 최종 선택한 문에 자동차가 있다면 (즉, 맞춘 경우), wins += 1: 이긴 횟수를 1 증가시킵니다.
4. 승률 계산 및 반환
return wins / n_games * 100
return wins / n_games * 100: 총 게임 횟수(n_games) 중에서 이긴 횟수(wins)의 비율을 계산하고, 100을 곱하여 백분율로 반환합니다.
5. 시뮬레이션 실행 및 결과 출력
# 시뮬레이션 실행 (각각 10만 번)
n = 10000
win_rate_stay = monty_hall_simulation(n, 'stay')
win_rate_switch = monty_hall_simulation(n, 'switch')
print(f"--- 몬티홀 시뮬레이션 결과 ({n:,} 회 실행) ---")
print(f"1. 선택을 안 바꿨을 때 승률: {win_rate_stay:. 1f}%")
print(f"2. 선택을 바꿨을 때 승률 : {win_rate_switch:. 1f}%")
n = 10000: 시뮬레이션을 10,000번 반복하도록 설정합니다.
win_rate_stay = monty_hall_simulation(n, 'stay'): '선택을 안 바꾸는' 전략으로 n번 시뮬레이션을 실행하고 승률을 win_rate_stay 변수에 저장합니다.
win_rate_switch = monty_hall_simulation(n, 'switch'): '선택을 바꾸는' 전략으로 n번 시뮬레이션을 실행하고 승률을 win_rate_switch 변수에 저장합니다.
print(...): 계산된 두 가지 전략의 승률을 보기 좋게 출력합니다. {n:,}는 숫자 n을 천 단위 구분 기호와 함께 출력하고, {win_rate_stay:. 1f}%는 win_rate_stay 값을 소수점 첫째 자리까지 표시하고 % 기호를 붙여 출력합니다.
이 코드를 실행하면, '선택을 바꾸는' 전략이 '선택을 안 바꾸는' 전략보다 승률이 훨씬 높다는 것을 확인할 수 있습니다. 이것이 바로 몬티홀 문제의 흥미로운 결론입니다!
코드를 실행하면 소름 돋는 결과가 나옵니다.
50 대 50이 아닙니다. 바꾸는 것이 이길 확률이 압도적으로(2배) 높습니다. 왜 그럴까요?
몬티홀 문제 풀이 완벽 총정리 (조건부확률 베이즈정리 증명)
비밀은 '조건부 확률'에 있습니다. 사회자가 문을 열어준 행위가 새로운 정보를 줬기 때문입니다.
처음에 여러분이 1번 문을 골랐을 때, 자동차가 거기 있을 확률은 1/3입니다. 반대로 말하면, 나머지 2번, 3번 문 어딘가에 자동차가 있을 확률은 2/3였습니다. (여러분이 꽝을 골랐을 확률이 훨씬 높죠.)
그런데 사회자가 3번 문을 열어서 "여기엔 없어(꽝)"라고 보여줬습니다. 그럼 아까 그 2/3의 확률 덩어리는 어디로 갈까요? 사라질까요? 아닙니다. 고스란히 남아 있는 2번 문으로 몰빵 됩니다.
내 문(1번): 여전히 1/3 (변함없음)
남의 문(2번+3번): 원래 2/3였는데, 3번이 꽝이니 2번 혼자서 2/3 (66.7%)를 독차지함.
그래서 무조건 2번으로 갈아타는 것이 유리한 것입니다. 사회자의 행동(조건)이 확률의 지형도를 바꿔버린 것이죠.
우리는 흔히 확률을 동전 던지기처럼 '고정된 값'이라고 생각합니다. 하지만 몬티홀 문제가 알려주는 진실은 다릅니다. "새로운 정보(사회자가 문을 염)가 들어오면, 확률은 변한다."
이것이 바로 우리가 앞으로 다룰 조건부 확률의 핵심이자, 2부에서 다룰 베이즈 정리의 씨앗입니다.
우리의 직관은 자주 틀립니다. 하지만 괜찮습니다. 우리에겐 파이썬이라는 든든한 계산기가 있으니까요. 다음 시간에는 이 원리를 이용해, "의사 선생님이 내린 암 진단이 틀릴 확률"을 계산해 보겠습니다. 내 목숨이 달린 문제라면, 직관보다는 계산을 믿어야 하지 않을까요?