본문 바로가기

취미코딩/et cetera

로또를 하면 안되는 이유를 파이썬으로 알아보기

어느 프로그램 언어든 초반에 이런 저런 내용을 기웃거려보다, random과 관련된 내용을 접할 때 쯤 로또 번호를 떠올릴 만하다. 내가 짠 코드로 지정한 로또 번호라면 좀 더 행운이 따르지 않을까 하는 소박한 신앙이다. 로또 번호를 연구한다는 사람도 있고, 추천 번호가 있다는 광고는 또 얼마나 많은가.

그런데 1에서 45사이에서 무작위로 6개의 숫자를 고르는 코드는 매우 쉽다. 그리고 그걸 로또 구입에 써먹는다고 달라지는 건 본인의 기대감 뿐이다. 코드도 재미없다.

그러니, 역으로 "복권은 통계에 무지한 자들이 내는 추가 세금이다"라는 격언이 맞는 지를 코드로 시험해보자.

엄밀히 말하면 진짜 random이라는 건 없다. 굳이 어렵게 생각해보지 않아도, 정해진 논리에 따라 값을 내도록 설계된 프로그래밍 언어가 논리가 없이, 개연성이 없이 숫자를 지정할 수 있는 방법은 없다. 따지고 보면 우리가 머리 속으로 로또 번호를 무작위로 정한다고 생각하지만, 그게 정말 무작위인지 잠재의식 속에 남은 어떤 원인으로 인해 연상한 번호인지 모른다.

파이썬의 랜덤은 '의사 난수'(pseudo-random numbers)라고 부른단다. 무작위이긴 무작위인데, 인간 입장에서 무작위라고 느낄만큼 적당히 복잡한 과정을 거쳐 '정해진' 숫자를 내보낸다는 뜻이다. 메르센 트위스터(Mersenne Twister)라는 방식으로 만들며, 난수라고 느낄 정도로 잘 작동하지만 암호화에 사용할 정도는 아니라고 한다.

랜덤 모듈은

 

import random

으로 호출한다. 메소드는 여러 개인데 하나만 쓸거다.

1~45사이 로또 번호를 추출하는 건 여러가지 방법이 있는데(한 줄로 끝나는 것도 있다), 예전에 본 복권번호 추첨장면이 떠올라 비슷하게 했다.

def spin():
    pool = list(range(1, 46))
    
    wnums = []

    for _ in range(6):
        wnum = random.choice(pool)
        wnums.append(wnum)
        pool.pop(pool.index(wnum))
    
    wnums.sort()

    return tuple(wnums)

먼저 1~45까지 정수를 순회하는 range를 리스트로 변환했다. 그래야 하나씩 꺼낼 수 있는 pop 메소드를 쓸 수 있으니까.

6개의 숫자를 담을 빈 리스트를 생성한다.

random.choice()는 인자로 리스트를 받아서 값 하나를 임의로 지정한다. 빈 리스트에 하나씩 추가한뒤, 해당 원소는 제외한다. 이 과정을 6번 반복한다.

추첨된 6개의 숫자는 오름차수으로 정렬한 뒤, 튜플로 변환해 반환한다. 튜플로 변환하는 이유는 나중에 써먹을 데가 있어서다.

로또 번호를 파이썬에게 추천받고 싶은 이들에겐 여기까지가 끝일 것이다. 이 다음부터는 이게 왜 소용없는지를 검증하는 단계다.

로또는 보통 1장씩 산다. 1장에는 6개의 번호가 5묶음 들어간다. 한 장에 5000원이다. 그리고 보통은 자동으로 산다.

def draw(piece:int=1, winning_num:tuple = (9, 18, 19, 30, 34, 40)):
    
    lotto_count = 5 * piece

    lotto = []

    for _ in range(lotto_count):
        lotto.append(spin())

    win = lotto.count(winning_num)

    if win:
        print("당첨!")
    else:
        print("낙첨")
        print(f"잃은 돈: {piece*5000}")
        print("내 번호")
        for l in lotto:
            print(l)

번호를 맞추는 함수에는 인자를 2개 넣었다. 1장 단위로 입력할 수 있게 했고, 지난 당첨번호를 샘플로 넣었다.

1장에 5묶음이니 내 번호는 5번 반복해서 spin 함수를 호출해 담는다.

리스트의 count 메소드로 당첨번호가 있는지 확인한다(이 부분을 간단하게 하려고 spin 함수에서 튜플로 반환했다)

조건문 결과는 본인이 기뻐할 만한 문구를 마음껏 집어넣자.

굳이 실행하지 않아도 win = True인 상황은 거의 발생하지 않을 걸 알고 있을 것이다. 그게 로또 1등 당첨을 꿈꾸며 잠시 행복한 현실도피를 하는 위안을 갉아먹지는 않았으면 좋겠다. 다만 로또에 쏟는 돈을 늘리거나 광고에 혹하지는 않는데 도움이 되면 됐다.

써놓고 보니 좀 허무하다. 좀 더 돌려보고 싶다. 1~45개에서 내가 찍은 번호 6개가 나올 확률은

(6 / 45) * (5 / 44) * (4 / 43) * (3 / 42) * (2 / 41) * (1 / 40)

이다. 8,145,060분의 1이며, 0.000012277380399898834%다. 모두 다른 번호로 산다고 해도 1629012장이 필요하다.

수동 구입으로 모든 번호를 다르게 구성해 약 162만 장을 살 수는 없을 것이다. 자동구입으로 최대한 많이 구입한다고 해보자. 로또를 한 장씩 계속 샀을 때 몇 번째 장에서 당첨이 될까. 이게 결과가 나오긴 할까.

일단 짜보자. 우선 draw 함수의 조건문을 다음과 같이 수정하자.

    if win:
        return True
    else:
        return False

그리고 저걸 당첨될 때까지 반복할 함수를 짜보자.

def untilwin(limit:int = 1629012):
    count = 1

    lotto = draw()

    while not lotto and count < limit:
        count += 1
        lotto = draw()
    
    print(count, lotto)

실제 로또를 산다면 마지막 한 장까지 확인할테니 마지막에 lotto를 출력해서 낙첨인지 당첨인지 확인해봤다.

162만9012장으로는 딱 두 번 실행해봤는데 첫 번째에서는 67만9083번째 장에서 당첨됐다. 두 번째에서는 낙첨됐다.

마지막으로 하나만 더 해보자. 믿음의 차원에서 지난 주 당첨번호로만 테스트하는게 맘에 안들 수도 있다. 매주 로또에 쏟는 예산을 정하고, 매주 그 만큼 로또를 샀을 때 당첨될 확률을 구해보자. 100년이 5214주 정도 되니 5000회 이하로 제한하자. 

def weekly_draw(money:int = 100000):
    pieces = money // 5000

    winnin_numbers = spin()

    for _ in range(pieces):
        lotto = []
        for _ in range(5):
            lotto.append(spin())
        if lotto.count(winnin_numbers):
            return True
        else:
            continue
    return False

def mustwin(limit:int = 5000):
    week = 1

    lotto = weekly_draw()

    while not lotto and week < limit:
        week += 1
        lotto = weekly_draw()
    
    print(week, round(week*7/365, 1))

매주 10만원 씩 구입한다고 하고(1만원 이상 사본적이 없지만 주위에 그런 사람은 봤다), 5번 실행결과 의외로 첫 번째에 360주, 약 6.9년만에 당첨이 됐다. 세 번째 실행결과에서는 26.4년만에 당첨됐다. 나머진 모두 낙첨됐다. 5번 더 실행했지만 낙첨됐다.

매주 주말 겪는 현타를 이제 원할 때마다 겪을 수 있게 됐으니, 굳이 살 필요가 없게 됐다.