#think-bayes #probability #study


이번 장에서는 랜덤 간격(random interval)으로 발생하는 이벤트를 설명하는 데 사용되는 모델인 포아송 프로세스를 소개한다. 푸아송 과정의 예로, 우리가 흔히 '풋볼'이라고 부르는 미국식 영어인 축구의 골 득점을 모델링한다. 게임에서 득점한 골을 사용해 푸아송 과정의 매개 변수를 추정한 다음, posterior 분포를 사용하여 예측한다.

그리고 월드컵 문제를 풀어본다.

The World Cup Problem

2018 FIFA 월드컵 결승전에서 프랑스는 크로아티아를 4골 2대 2로 꺾었다.

이러한 질문에 답하기 위해 모델링을 결정해야한다.

물론 이러한 가정 중 어느 것도 현실 세계에서 완전히 맞진 않는다. 조지 박스가 말했듯이 "모든 모델은 틀렸지만 일부는 유용합니다." (https://en.wikipedia.org/wiki/All_models_are_wrong).

위 경우 이러한 가정이 사실이라면 적어도 대략적으로나마 게임에서 득점한 골의 수가 푸아송 분포를 따르기 때문이다.

The Poisson Distribution

게임에서 득점한 골 수가 골 득점률 𝜆 인 포아송 분포를 따르는 경우, 𝑘 골을 득점할 확률은 다음과 같다.
(음수가 아닌 모든 값에 대해 𝑘)

λkexp(λ) / k!

SciPy 는 푸아송 분포를 나타내는 푸아송 객체를 제공한다. 다음과 같이 𝜆=1.4로 푸아송 객체를 만들 수 있다.

>>> from scipy.stats import poisson
>>> 
>>> lam = 1.4
>>> dist = poisson(lam)
>>> type(dist)
<class 'scipy.stats._distn_infrastructure.rv_discrete_frozen'>

"frozen" 무작위 변수를 나타내는 객체가 생성되고 포아송 분포의 pmf를 제공한다.

>>> k = 4
>>> dist.pmf(k)
0.039471954028253146

다음 함수를 사용하여 푸아송 분포를 나타내는 Pmf 를 생성해보자.

from empiricaldist import Pmf

def make_poisson_pmf(lam, qs):
    """Make a Pmf of a Poisson distribution."""
    ps = poisson(lam).pmf(qs)
    pmf = Pmf(ps, qs)
    pmf.normalize()
    return pmf

예를 들어, 다음은 0에서 9까지의 k 값에 대해 계산된 lam=1.4에 대한 골 득점 분포는 다음과 같다.

>>> import numpy as np
>>> 
>>> lam = 1.4
>>> goals = np.arange(10)
>>> pmf_goals = make_poisson_pmf(lam, goals)

반대로 골 수가 주어지면 골 득점률에 대해 무엇을 말할 수 있을까?
이 질문에 답하기 위해서는 점수를 보기 전에 가능한 범위와 그 확률을 나타내는 람다(lam)의 사전 분포에 대해 생각해 볼 필요가 있다.

The Gamma Distribution

축구 경기를 본 적이 있다면 람다에 대한 정보를 알고 있다. 대부분의 게임에서 팀은 몇 골씩 득점한다. 드물게 한 팀이 5골 이상을 득점하는 경우도 있지만 10골 이상을 득점하는 경우는 거의 없다.

이전 월드컵 데이터를 사용하여 각 팀이 경기당 평균적으로 약 1.4골을 득점하는 것으로 추정한다. 따라서 람다의 평균을 1.4로 설정할 것이다. 못하는 팀이 잘하는 팀과의 경기에서는 실점이 더 많을 것으로 예상하고, 잘하는 팀이 못하는 팀과의 경기에서 실점이 더 적을 것으로 예상한다.

골 득점률의 분포를 모델링하기 위해 감마 분포를 사용할 거이다.

SciPy는 감마 분포를 나타내는 객체를 생성하는 감마를 제공한다. 그리고 감마 객체는 감마 분포의 확률 밀도 함수 pdf를 제공한다.

>>> from scipy.stats import gamma
>>> 
>>> alpha = 1.4
>>> qs = np.linspace(0, 10, 101)
>>> ps = gamma(alpha).pdf(qs)

이를 정규화하려면 Pmf에 넣고 normalize를 호출한다.

>>> from empiricaldist import Pmf
>>> 
>>> prior = Pmf(ps, qs)
>>> prior.normalize()

그리고 평균이 약 1.4라는 것을 확인할 수 있다.

>>> prior.mean()
1.4140818156118378

다음은 업데이트를 해본다.

The Update

골 득점률 𝜆 이 주어지고 여러 골을 넣을 확률 𝑘 을 계산해 달라는 요청을 받았다고 가정해보자.
예를 들어 𝜆가 1.4이면 한 경기에서 4골을 넣을 확률은 다음과 같다.

>>> lam = 1.4
>>> k = 4
>>> poisson(lam).pmf(4)
0.039471954028253146

이제 𝜆에 대해 값의 배열이 있다고 가정하면, 다음과 같이 각 가상 값 lam에 대한 likelihood 을 계산할 수 있다.

>>> lams = prior.qs
>>> k = 4
>>> likelihood = poisson(lams).pmf(k)
>>> likelihood
array([0.00000000e+00, 3.77015591e-06, 5.45820502e-05, 2.50026149e-04,
       7.15008049e-04, 1.57950693e-03, 2.96358283e-03, 4.96792214e-03,
       7.66854765e-03, 1.11145981e-02, 1.53283100e-02, 2.03065231e-02,
       2.60231799e-02, 3.24324189e-02, 3.94719540e-02, 4.70665182e-02,
       5.51312092e-02, 6.35746276e-02, 7.23017337e-02, 8.12163834e-02,
       9.02235222e-02, 9.92310359e-02, 1.08151269e-01, 1.16902230e-01,
       1.25408499e-01, 1.33601886e-01, 1.41421844e-01, 1.48815687e-01,
       1.55738624e-01, 1.62153659e-01, 1.68031356e-01, 1.73349519e-01,
       1.78092787e-01, 1.82252178e-01, 1.85824592e-01, 1.88812285e-01,
       1.91222339e-01, 1.93066121e-01, 1.94358757e-01, 1.95118616e-01,
       1.95366815e-01, 1.95126749e-01, 1.94423652e-01, 1.93284180e-01,
       1.91736036e-01, 1.89807621e-01, 1.87527718e-01, 1.84925212e-01,
       1.82028837e-01, 1.78866956e-01, 1.75467370e-01, 1.71857150e-01,
       1.68062503e-01, 1.64108654e-01, 1.60019753e-01, 1.55818804e-01,
       1.51527608e-01, 1.47166728e-01, 1.42755463e-01, 1.38311841e-01,
       1.33852618e-01, 1.29393291e-01, 1.24948121e-01, 1.20530156e-01,
       1.16151272e-01, 1.11822205e-01, 1.07552602e-01, 1.03351061e-01,
       9.92251884e-02, 9.51816428e-02, 9.12261916e-02, 8.73637629e-02,
       8.35984979e-02, 7.99338039e-02, 7.63724057e-02, 7.29163965e-02,
       6.95672867e-02, 6.63260522e-02, 6.31931800e-02, 6.01687122e-02,
       5.72522885e-02, 5.44431858e-02, 5.17403564e-02, 4.91424640e-02,
       4.66479169e-02, 4.42549001e-02, 4.19614048e-02, 3.97652554e-02,
       3.76641357e-02, 3.56556121e-02, 3.37371552e-02, 3.19061603e-02,
       3.01599651e-02, 2.84958667e-02, 2.69111367e-02, 2.54030347e-02,
       2.39688206e-02, 2.26057656e-02, 2.13111624e-02, 2.00823331e-02,
       1.89166374e-02])

posterior 를 구하려면 prior 에 방금 계산한 확률을 곱하고 결과를 정규화하면 된다! 이를 캡슐화한 함수를 살펴보자.

def update_poisson(pmf, data):
    """Update Pmf with a Poisson likelihood."""
    k = data
    lams = pmf.qs
    likelihood = poisson(lams).pmf(k)
    pmf *= likelihood
    pmf.normalize()

이 예제에서는 프랑스가 4골을 넣었으므로 이전 데이터를 복사하여 업데이트 한다.

>>> france = prior.copy()
>>> update_poisson(france, 4)
>>> france
0.0     0.000000
0.1     0.000003
0.2     0.000053
0.3     0.000260
0.4     0.000755
          ...   
9.6     0.000009
9.7     0.000008
9.8     0.000007
9.9     0.000006
10.0    0.000005
Name: , Length: 101, dtype: float64

마찬가지로 크로아티아 posterior 분포를 구해본다.

>>> croatia = prior.copy()
>>> update_poisson(croatia, 2)
>>> croatia
0.0     0.000000e+00
0.1     1.154111e-03
0.2     4.987243e-03
0.3     1.080490e-02
0.4     1.764472e-02
            ...     
9.6     3.699161e-07
9.7     3.104885e-07
9.8     2.605416e-07
9.9     2.185748e-07
10.0    1.833229e-07
Name: , Length: 101, dtype: float64

프랑스와 크로아티아의 posterior 분포 평균을 구하면 다음과 같다.

>>> print(croatia.mean(), france.mean())
1.6999765866755225 2.699772393342308

이러한 결과는 베이지안 업데이트의 전형적인 결과로, posterior 분포 위치는 prior 분포와 데이터의 절충점이다. (compromise)

Probability of Superiority

이제 각 팀에 대한 posterior 분포를 알았으니 첫 번째 질문에 답할 수 있다

이 모델에서 "더 나은"이란 상대팀에 대해 더 높은 골 득점률을 갖는 것을 의미한다. posterior 분포를 사용하여 프랑스의 분포에 무작위로 추출한 값이 크로아티아의 분포에서 추출한 값을 초과할 확률을 계산할 수 있다.

이를 수행하는 한 가지 방법은 두 분포의 모든 값 쌍을 열거하고 한 값이 다른 값을 초과할 총 확률을 더하는 것이다.

def prob_gt(pmf1, pmf2):
    """Compute the probability of superiority."""
    total = 0
    for q1, p1 in pmf1.items():
        for q2, p2 in pmf2.items():
            if q1 > q2:
                total += p1 * p2
    return total

>>> prob_gt(france, croatia)
0.7499366290930155

Pmf 는 동일하게 prob_gt 함수를 제공한다.

>>> Pmf.prob_gt(france, croatia)
0.7499366290930174

(Pmf.prob_gt는 루프가 아닌 배열 연산자를 사용하기 때문에 결과가 약간 다르다고 한다.)

어느 쪽이든 결과는 75% 에 가깝다. 따라서 한 경기를 기준으로 볼 때 프랑스가 실제로 더 나은 팀이라고 어느 정도 확신할 수 있다.

물론 이 결과는 골 득점률이 일정하다는 가정을 전제로 한 결과라는 점을 기억해야 한다. 실제로는 한 골 차로 뒤지고 있는 팀이 경기 막판으로 갈수록 더 공격적으로 플레이하여 득점할 가능성도 높아지지만 추가 골을 내줄 가능성도 높아질 수 있다.

Predicting the Rematch

같은 팀이 다시 경기를 한다면 크로아티아가 이길 확률은 얼마나 될까?
이 질문에 답하기 위해 한 팀이 득점할 것으로 예상되는 골 수인 'posterior predictive 분포'를 생성한다.

골 득점률 lam을 알고 있다면, 골 분포는 매개 변수 lam 을 가지는 포아송 분포가 될 것이다. lam 을 모르기 때문에 골 분포는 다양한 값의 lam을 가진 푸아송 분포가 혼합된(mixture) 형태이다.

먼저 lam의 각 값에 대해 하나씩 Pmf 객체 시퀀스를 생성한다.

>>> pmf_seq = [make_poisson_pmf(lam, goals) 
	           for lam in prior.qs]

predictive 분포는 이러한 Pmf 객체들의 mixture 로, posterior 확률로 가중치가 부여됩니다. mixture 계산을 위해 make_mixture를 이용할 것이다.

>>> from utils import make_mixture
>>> 
>>> pred_france = make_mixture(france, pmf_seq)
>>> pred_france
0    0.112030
1    0.201777
2    0.215467
3    0.177493
4    0.124612
5    0.078434
6    0.045603
7    0.024969
8    0.013047
9    0.006568


이 분포는 두 가지 불확실성의 원천을 나타낸다.

크로아티아의 predictive dist 는 다음과 같다.

>>> pred_croatia = make_mixture(croatia, pmf_seq)
>>> pred_croatia
0    0.251958
1    0.285581
2    0.209489
3    0.125770
4    0.067152
5    0.033190
6    0.015534
7    0.006984
8    0.003045
9    0.001297

이러한 분포를 사용하여 프랑스가 재경기에서 승리, 패배 또는 무승부를 거둘 확률을 계산할 수 있다.

>>> win = Pmf.prob_gt(pred_france, pred_croatia)
>>> win
0.5703522415934519
>>> lose = Pmf.prob_lt(pred_france, pred_croatia)
>>> lose
0.26443376257235873
>>> tie = Pmf.prob_eq(pred_france, pred_croatia)
>>> tie
0.16521399583418947

프랑스가 동점 상황에서 절반을 이긴다고 가정할 때, 프랑스가 재경기에서 승리할 확률은 약 65% 이다.

>>> win + tie/2
0.6529592395105466

이는 75% 인 확률보다 약간 낮은 수치다. 골 득점률보다 한 경기의 결과에 대한 확신이 낮기 때문에 이는 당연한 결과다. 프랑스가 더 나은 팀이라고 해도 경기에서 질 수도 있다.

The Exponential Distribution

이 노트의 마지막에 있는 연습 문제에서는 월드컵 문제를 변형한 다음 문제를 풀어볼 수 있다.

2014 FIFA 월드컵에서 독일은 브라질과 준결승전을 치렀습니다. 독일은 11분 후와 23분에 다시 한 번 득점했습니다. 이 경기에서 독일이 90분 후에 몇 골을 넣을 것으로 예상하십니까? 독일이 5골을 더 넣을 확률은 얼마였나요(실제로 그렇게 되었음)?

이 버전에서는 데이터가 정해진 기간 동안의 골 수가 아니라 골 사이 시간이라는 점에 유의해야한다.

이와 같은 데이터의 likelihood 을 계산하기 위해 포아송 프로세스 이론을 다시 활용할 수 있습니다. 각 팀의 골 득점률이 일정하다면, 골 사이의 시간은 지수 분포를 따를 것으로 예상할 수 있다.

골 득점률이 𝜆인 경우, 골 간격이 𝑡일 확률은 지수 분포 PDF 에 비례한다.

𝜆exp(𝜆𝑡)

𝑡는 연속적인 양이므로 이 식의 값은 확률이 아니라 확률 밀도이다. 하지만 데이터의 확률에 비례하므로 베이지안 업데이트에서 likelihood 로 사용할 수 있다.

SciPy는 지수 분포를 나타내는 객체를 생성하는 지수 함수를 제공한다. 그러나 예상하는 방식으로 lam 을 매개변수로 사용하지 않기 때문에 작업하기가 어색하다.

def expo_pdf(t, lam):
    """Compute the PDF of the exponential distribution."""
    return lam * np.exp(-lam * t)

지수 분포가 어떻게 생겼는지 알아보기 위해 다시 한 번 lam이 1.4라고 가정하면 다음과 같이 𝑡의 분포를 계산한다.

>>> lam = 1.4
>>> qs = np.linspace(0, 4, 101)
>>> ps = expo_pdf(qs, lam)
>>> pmf_time = Pmf(ps, qs)
>>> pmf_time.normalize()
25.616650745459093

Summary

이 장에서는 세 가지 분포을 소개하므로 정리하기 어려울 수 있다.

감마 분포를 선택한 이유는 그 모양이 골 득점률에 대한 배경 지식과 일치하기 때문이다. 다른 분포를 사용할 수도 있지만, ConjugatePriors 에서 감마 분포가 특히 좋은 선택이 될 수 있음을 살펴볼 것이다.