Paradoks dnia urodzin to zjawisko probabilistyczne, które wydaje się sprzeczne z intuicją. Polega na tym, że w grupie osób istnieje zaskakująco wysokie prawdopodobieństwo, że co najmniej dwie osoby będą miały urodziny tego samego dnia. Przykładowo, w grupie 23 osób prawdopodobieństwo wynosi aż 50,7%, a w grupie 70 osób – aż 99,9%. W tym artykule wyjaśnimy, jak działa ten paradoks, i pokażemy, jak napisać program w Pythonie, który symuluje to zjawisko metodą Monte Carlo.
Aby lepiej zrozumieć prawdopodobieństwo w paradoksie dnia urodzin, napiszemy program wykorzystujący metodę Monte Carlo. Ta metoda polega na przeprowadzeniu wielu losowych symulacji, które pomagają oszacować prawdopodobieństwo, zamiast obliczać je analitycznie.
Fukcja losująca dni urodzin
Na pewno będziemy losować dni urodzin, dlatego będziemy potrzebowali zaimportować moduł random, a skoro pracujemy z datami to przyda się też moduł datetime. Utworzymy też funkcję od razu i użyjemy tzw. docstring do jej opisania. Jest to po prostu tzw. string wielowierszowy, który często jest stosowany do opisania funkcji, klas czy modułów. Docstring może stać się częścią dokumentacji po prostu.

W funkcji getBirthdays tworzymy tablicę, by następnie uruchomić pętlę, która będzie nam zaczynała rok od początku i losowała z niego jakiś dzień. Dodajemy wylosowaną liczbę do początku roku, by automatycznie uzyskać datę, a następnie przypisujemy nią do utworzonej wcześniej tablicy. Dalej funkcja zwróci tablicę dla swojego miejsca wywołania, gdzie pewnie zapiszemy tablicę wylosowanych dni do zmiennej. Zaskoczeniem może być użycie timedelta z modułu datetime. Metoda ta tworzy po prostu interwał czasowy z wylosowanej liczby by można było ją łatwo dodać do daty początku roku.
Porównywanie dni urodzin w tablicy
Teraz zajmiemy się funkcją, która będzie zwracać datę urodziny pojawiającą się więcej niż raz w liście. Jak to zrobimy? Na początek nazwiemy funkcję - nadamy jej nazwę getMatch, przyjmie jako pametr listę.

No i zaczniemy od wyeliminowania przypadku gdy nie ma dwu osób o tej samej dacie urodzin. Zrobimy to prosto - porównamy długość listy (uzyskaną funckją len), z długością listy zamienionej na set (przy zamianie nie będzie duplikatów). Jeśli takie porównanie będzie prawdziwe to znaczy, że nie ma powtórzeń . Następnie możemy sobie poprzedlądać listę w pętli. By sobie to uprościć dalsze kroki (będziemy musieli porównywać listę z samą sobą) użyjemy enumerate.
Funkcja enumerate zwróci nam listę, w której będziemy mieli pary indeks i wartość danego elementu. Mamy tu zatem dwie wartości, stąd użyjemy w pętli dwu zmiennych. Zmienna a to będzie nasz indeks, a zmienna birthdayA będzie zawierała wartośc jaka znajduje się na danym indeksie.
Nastepnym krokiem będzie stworzenie kolejnej pętli, która będzie iterować po tym samym. Zrobi to zaczynając od elementu o jeden późniejszego od wcześniejszej pętli. Tutaj dzięki użyciu slicingu, uzyskamy elementy od indeksu listy a +1 do samego końca listy. No i tutaj dochodzimy do mmentu gdzie w birthdayA mamy datę, a w birthdayB datę występującą po niej w liście. Możemy zatem je porównać, jeśli są sobie równe to zwracamy którąś, ja zwracam pierwszą z nich.
Paradoks dnia urodzin główna część kodu
Następnie możemy przejść do głównej części kodu, a tutaj na początek poinformujmy użytkownika co za program, w ogóle, włączył. Następnie przyda się jakaś stała z miesiącami by wyświetlać ich nazwy.

Po załatwieniu tych formalności możemy zacząć zabawę. Początek dość standardowy bo tworzymy pętlę, która będzie pytać o ilość urodzin do wygenerowania (nie więcej niż 100, by user nie przesadził). W tym while następnie przyjmujemy odpowiedź i sprawdzamy czy jest liczbą i czy mieści się w przedziale. Jeśli dane są poprawne to przerywamy i idziemy dalej z programem. Jeśli użytkownik jednak nie poda poprawnych danych to nie zostanie wypuszczony z nieskończonej pętli.

Następnie mamy część kodu, która będzie odpowiedzialna za poważne sprawy czyli generowanie urodzin i wyświetlanie. Mamy tutaj zatem komunikat, że wygenerowano tyle dat ile podał user. Następnie te daty rzeczywiście generujemy i zapisujemy w zmiennej birthdays. Następnie mamy pętlę, która znowu uskutecznia trick z enumerate. Iterujemy więc po parach indeksów i ich wartości. Jeśli indeks jest różny od zera, to wyświetlamy przecinek, jeśli nie (czyli to pierwszy element) to nie wyświetlamy. Po przecinku bierzemy sobie nazwę miesiąca (listy zaczynamy od zera, więc odejmujemy 1). Następnie konstruujemy komunikat w zmiennej dateText, w którym umieszczamy nazwę miesiąca i dzień z tablicy z datami. Na koniec pętli wszystko wyświetlamy, nie przechodząc jednak do kolejnej linii.
Wyświetlanie wyników i symulacje - paradoks dnia urodzin
Teraz będziemy konstruować komunikat o wyniku tej początkowej symulacji.

Na początek wyświetlimy pierwszą część komunikatu, a najwyżej doczepimy resztę dynamicznie. W następnej linii sprawdzimy czy zmienna match coś zawiera. Powinna zawiarać coś jeśli getMatch zwróci wartości. Jeśli natomiast match jest pusta, to nie ma osób o takim samym dniu urodzin, a jeśli tak się stanie, to dopisujemy (w elsie), że nie ma.
Natomiast jeśli match nie jest równy None, czyli są jakieś osoby urodzone tego samego dnia roku, to do dateText wędruje nazwa miesiąca i dzień. Następnie mając nazwę miesiąca i dzień, które kilka osoób ma takie same, dopisujemy 'kilka osób ma urodziny' i tą datę.
Więcej symulacji paradoksu dnia urodzin
Teraz będziemy się bawić w symulacje masowo, by obliczyć dokładniej prawdopodobieństwo.

Będziemy robić symulacje 100k razy, to powinno dać dość dobre wyniki prawdopodobieństwa. Na początek damy użytkownikowi szansę by uruchomić symulacje wciskając enter. Teraz następny komunikat, że bierzemy się do pracy. Tworzymy teraz zmienną simMatch, w której będziemy zapisywać czy mamy jakieś dopasowania.
Uruchamiamy w końcu pętlę for, która powtórzy się 100 tysięcy razy. Tutaj w kodzie mamy użyty podkreślnik, ale to tylko dla czytelności, Python wie o co chodzi. W końcu ten język został stworzony by być czytelnym, zatem fajny "ficzer". I teraz sprawdzamy czy po podzieleniu aktualnego numeru iteracji przez 10 000 zostaje reszta, bo jeśli nie zostaje to wyświetlamy komunikat, że mamy kolejne 10k przeprowadzonych symulacji. Robimy tak, by użytkownik wiedział, że program się nie zaciął.
Po komunikacie, generujemy losowe urodziny (tyle razy ile użytkownik wpisał). I sprawdzamy czy funkcja getMatch znajdzie dopasowania. Jeśli wspomniana funkcja zwróciła jakieś dopasowania, to dodajemy do zmiennej simMatch jeden by zapamiętać ile z tych wszystkich symulacji zwróciło dopasowania. Ten blok kodu kończymy komunikatem o przeprowadzeniu naszych 100k symulacji 🙂
Prawdopodobieństwo
Końcowy blok kodu naszego programu jest banalny. Tutaj po prostu wyświetlamy kolejno komunikaty z wartościami, używając przy tym prostej matematyki.

Spróbuj sam wymyślić, choć dużo myślenia tu nie ma, co tu się dzieje. Prześledź to i coś tutaj zmień. Pobaw się kodem jak zawsze, i podziel efektami w komentarzu 🙂 Teraz już wiesz na czym polega paradoks dnia urodzin i umiesz oszacować jego prawdopodobieństwo.

[…] tego przyda się kolejna pętla, ale też… wykorzystamy twojego znajomego, jeśli czytałeś poprzedni artykuł. Znowu użyjemy enumerate, które numeruje elementy pętli. Jeśli nie pamiętasz, zajrzyj do […]