개요

각 철도에는 이를 지나가는 기관차에 1부터 N까지의 순서로 번호를 붙인다. 어느날 60호 기관차를 보았다. 이 때 이 철도에는 몇 개의 기관차가 지나가는지 추측해보자.

풀이

가설 세우기

이 문제는 주사위 문제과 비슷하다.

주사위 문제의 풀이를 활용하기 위해 문제를 다음과 같이 생각할 수 있다.

  • 60 면체, 60 + 1 면체, 60 + 2 면체, … 60 + n 면체 주사위가 든 상자가 있다.
  • 상자에서 임의로 주사위 하나를 집어서 던졌더니 60이 나왔다.
  • 각 주사위를 선택했을 확률은?

가설과 데이터는 어떻게 될까?

  • n 개의 가설(Hypothesis)을 생각할 수 있다.
    • 가설 60 : 60면체 주사위를 던졌다.
    • 가설 61 : 61면체 주사위를 던졌다.
    • 가설 n : n 면체 주사위를 던졌다.
  • 데이터 D : 주사위를 던져 60이 나왔다.

표로 정리해 보면 다음과 같을 것이다.

설명
p(H60) n개의 주사위 중 60면체 주사위를 선택할 확률 1n
p(H61) n개의 주사위 중 61면체 주사위를 선택할 확률 1n
p(Hn) n개의 주사위 중 n면체 주사위를 선택할 확률 1n
p(H1000) 1000개의 주사위 중 n면체 주사위를 선택할 확률 11000
p(DH60) 60면체 주사위를 던져 60이 나올 확률 160
p(DH61) 61면체 주사위를 던져 60이 나올 확률 161
p(DHx) x면체 주사위를 던져 60이 나올 확률 1x
p(D) 주사위를 던져 60이 나올 확률 아직 모름
  • 편의상 철도 회사가 보유한 총 기관차의 수가 n이라 하자.
    • 즉, p(H60)은 철도 회사가 보유한 n 대의 기관차 중 우리가 목격한 하나의 노선 위를 지나는 기관차가 60대일 확률이다.
    • 즉, 우리가 구하려 하는 것은 총 기관차 수 n 중에서 관찰 대상인 노선에서 운행되는 기관차의 수이다.

p(D)를 구하자

p(D)의 개념은 다음과 같다.

p(D)= p(D and H60)+p(D and H61)+...+p(D and Hn)

따라서 60면체에서 m면체까지 주사위가 전부 m - 59 개 있을 때, p(D)는 다음과 같다.

p(D)= p(Hm59)p(DH60)+p(Hm59)p(DH61)+...+p(Hm59)p(DHm)= 1m59×160+1m59×161+...+1m59×1m= limm1m59(160+161+...+1m)=limm1m59mk=60k1

한편, mk=60k1조화수(Harmonic number) Hm 이므로 이를 써서 다시 표기해보면 다음과 같이 표현할 수 있다.

p(D)=limm1m59×(HmH59)

그런데, n이 너무 커지면 곤란하기도 하고, 현실의 열차가 무한히 많을 수는 없으니 편의상 1000 까지만 따져 보도록 하자.

p(D)=1100059×(H1000H59)

손으로 계산하기엔 시간이 너무 걸리므로 다음과 같이 간단한 자바스크립트 함수를 작성하여 계산해주자.

// 조화수 H_n 을 구한다
function harmonic(n) {
    if (n < 1) {
        throw "invalid argument";
    }
    if (harmonic[n] != null) {
        return harmonic[n];
    }
    if (n == 1) {
        return harmonic[1] = 1;
    }
    return harmonic[n] = harmonic(n - 1) + Math.pow(n, -1);
}
const p_d = (harmonic(1000) - harmonic(59)) / (1000 - 59);
console.log(p_d);

위의 코드를 실행해보면 다음과 같은 값이 출력된다.

p(D)0.0029992211628748922

사후 확률을 계산하자

이제 사후 확률을 계산할 수 있다.

베이즈 정리(Bayes' theorem)에 의해 사후 확률은 다음과 같다. (단, x60)

p(HnD)=p(Hn)×p(DHn)p(D)=1100059×1np(D)=1(100059)×n×p(D)=1941×n×p(D)

그렇다면 이제 모든 가설(열차가 60 대인 경우, 61 대인 경우, … n 대인 경우)을 비교하여 가장 가능성이 높은 가설을 찾아내기만 하면 된다.

즉, 다음 값들을 비교하여 가장 큰 값을 찾으면 된다.

p(H60D),p(H61D),...

그런데 n이 분모에 있으므로, n이 커질수록 수는 작아진다.

따라서 가능한 확률의 최대값이 나오는 n 은 60이 된다.

당연히 60대 중의 1대일 가능성61대 중의 1대일 가능성이나, 62대 중의 1대일 가능성보다 크기 때문이다.

코드를 짜서 사후확률을 계산해 보자

다음은 (study) 파이썬을 활용한 베이지안 통계의 코드를 참고하여 자바스크립트로 풀어본 것이다.

// hypos: 가설의 배열
// 가설의 배열을 돌며 같은 경우의 수 1을 부여한다
function init(hypos) {
    const dict = {};
    hypos.forEach((h) => {
        dict[h] = 1;
    });
    return dict;
}

// 모든 가설을 돌며 mix의 data에 해당하는 값을 곱해 업데이트한다
function update(dict, data) {

    Object.keys(dict).forEach((hypo) => {
        dict[hypo] = dict[hypo] * likelihood(hypo, data);
    });

    return normalize(dict);
}

// p(D | H_n)
function likelihood(hypo, data) {
    if (hypo < data) {
        return 0;
    }
    return 1 / hypo;
}

// 모든 가설의 확률의 비율을 유지하며, 총합이 1이 되도록 정규화한다
function normalize(dict) {
    const values = Object.values(dict);
    const sum = values.reduce((a, b) => a + b);
    const result = {};
    Object.keys(dict).forEach((key) => {
        result[key] = dict[key] / sum;
    });
    return result;
}

function range(start, size) {
    return [...Array(size).keys()].map((n) => n + start);
}

function main() {
    const hypos = range(1, 1000);
    const pmf = init(hypos);
    const result = update(pmf, 60);

    Object.keys(result).forEach((k) => {
        console.log(k + '\t' + result[k])
    });
}

main();

위의 코드를 실행해서 엑셀에 붙여넣고 차트를 그려보면 다음과 같이 나온다.

$ node train.js | pbcopy

train-graph

기대값을 구해보자

하지만 원하는 값은 이런 결과가 아니므로 관점을 바꾸어 기대값, 즉 사후 확률의 평균을 구해 보도록 하자.

60×p(H60D)+61×p(H61D)+...+1000×p(H1000D)=60941×60×p(D)+61941×61×p(D)+...+1000941×1000×p(D)=1941×p(D)+1941×p(D)+...+1941×p(D)=941×1941×p(D)=1p(D)10.0028222671142652737333.41989326370776

즉, 333대의 열차가 있을 것이라고 기대할 수 있다.

한편 위의 자바스크립트 코드 중 main함수만 다음과 같이 수정해주면 기대값을 구할 수 있다.

function main() {
    const hypos = range(1, 1000);
    const pmf = init(hypos);
    const result = update(pmf, 60);

    Object.keys(result).forEach((k) => {
        console.log(k + '\t' + result[k])
    });

    // 기대값 rs를 구한다
    const result2 = normalize(result);
    const rs = Object.keys(result2).reduce((a, b) => {
        return a + (b * result2[b])
    }, 0);
    console.log(rs);
}

위의 코드를 실행해보면 333.41989326371095가 나온다.

기대값을 구하는 공식을 만들어 보자

1p(D)를 이용하면 되므로 공식으로도 만들 수 있을 것 같다.

  • a : 목격한 열차의 번호
  • b : 임의의 열차 번호 예상 최대값(위에서 1000으로 사용한 값)
1p(D)=(1b(a1)×(HbHa1))1=b(a1)bk=ak1

WolframAlpha에 물어보자

공식을 만들었으니, 언제든지 WolframAlpha에 물어볼 수 있다.

사전 확률로 할 수 있는 것

균등 분포가 1 ~ 1000 일 때, 사후 확률의 평균은 333이 나왔다.

상한값이 500이라면 사후 확률 평균은 207일 것이고, 상한값이 2000이라면 사후 확률 평균은 552일 것이다.

function main(max) {
    const hypos = range(1, max);
    const pmf = init(hypos);
    let result = update(pmf, 60);

    const result2 = normalize(result);
    const rs = Object.keys(result2).reduce((a, b) => {
        return a + (b * result2[b])
    }, 0);
    console.log(rs);
}
main(500);  // 207.07922798340903
main(2000); // 552.179017164631

이런 방식으로는 상한값이 커질수록 사후 확률 평균도 커지게 되므로 바람직하지 않다.

따라서 데이터를 추가로 확보하면서 값이 조정되도록 해야 한다.

책에서는 60호 기관차에 이어 30번, 90번 기관차도 본 경우를 제시한다.

아마도 이런 경우의 사후 확률은 다음과 같이 식을 세우면 될 것 같다.

p(D|H60)×p(D|H30)×p(D|H90)

그렇다면 다음과 같이 계산할 수 있을 것이다.

function main(max) {
    const hypos = range(1, max);
    const pmf = init(hypos);

    // 60, 30, 90번 기관차를 목격
    let result = update(pmf, 60);
    result = update(pmf, 30);
    result = update(pmf, 90);

    const result2 = normalize(result);
    const rs = Object.keys(result2).reduce((a, b) => {
        return a + (b * result2[b])
    }, 0);
    console.log(rs);
}

main(500);  // 151.84958795903844
main(1000); // 164.30558642273365
main(2000); // 171.33818109150928

30, 90번 기관차를 본 데이터의 영향을 받아 사후 확률의 평균들의 차이가 줄었음을 알 수 있다.

상한선 60번 열차 사후평균 60, 30, 90 사후평균
500 약 207.08 151.85
1000 약 333.42 164.31
2000 약 552.18 171.34

사전 확률의 대안

  • 위와 같은 상황에서는 가급적이면 신뢰할 수 있는 데이터를 많이 수집하는 것이 좋다.
  • 철도 회사의 규모를 알아내기 위해 기관차 운송 분야 전문가와 인터뷰를 하는 것도 방법.

여기에서는 로버트 엑스텔(Robert Axtell)의 멱법칙을 사용해보도록 하자.

멱법칙 : 주어진 규모의 회사 크기는 규모의 분포의 역수이다.

PMF(x)(1x)a

멱법칙 사전 확률을 고려하면 다음과 같이 init함수를 수정할 수 있다.

function init(hypos) {
    const dict = {};
    const alpha = 1
    hypos.forEach((h) => {
        dict[h] = Math.pow(h, -alpha);
    });
    return dict;
}

그리고 다음 코드를 다시 실행해보면 다음과 같은 결과가 나온다.

main(500);  // 130.70846986255998
main(1000); // 133.27523137503073
main(2000); // 133.99746308073068
상한선 60 사후평균 60,30,90 사후평균 멱법칙 적용 60,30,90
500 약 207.08 약 151.85 130.71
1000 약 333.42 약 164.31 133.28
2000 약 552.18 약 171.34 134.00

차이가 더 줄어들었음을 알 수 있다.

시험삼아 멱법칙 적용 코드에 hypos 값으로 2000000을 넣어봤더니 134.25418932762943가 나왔다.

숫자가 더 커져도 변동폭은 크지 않을 것으로 보인다.

신뢰구간 적용해보기

이번에는 위에서 계산한 분포 목록을 사용하여 90% 신뢰구간을 구해보자.

5분위와 95분위 값을 계산하면, 90% 신뢰구간을 구할 수 있다.

이미 normalize를 했기 때문에 그대로 순서대로 더해주기만 하면 되겠다.

  • 5분위 : 0~5% 에 해당하는 값을 모두 더해주면 된다.
  • 95분위 : 0~95% 에 해당하는 값을 모두 더해주면 된다.

다음과 같이 main 함수만 조금 고쳐주면 된다.

function main(max) {
    const hypos = range(1, max);
    const pmf = init(hypos);

    let result = update(pmf, 60);
    result = update(pmf, 30);
    result = update(pmf, 90);

    const result2 = normalize(result);

    const keys = Object.keys(result2);

    let p5 = 0;
    let p5total = 0;
    for(let i = 0; i < keys.length; i++) {
        const val = keys[i];
        const prob = result2[val];
        p5total += prob;
        if (p5total >= 0.05) {
            p5 = val;
            break;
        }
    }

    let p95 = 0;
    let p95total = 0;
    for(let i = 0; i < keys.length; i++) {
        const val = keys[i];
        const prob = result2[val];
        p95total += prob;
        if (p95total >= 0.95) {
            p95 = val;
            break;
        }
    }
    console.log({ '5%': p5, '95%': p95});
}

위의 코드를 실행해보면 다음과 같은 결과가 나온다.

main(500);  // { '5%': '91', '95%': '235' }
main(1000); // { '5%': '91', '95%': '242' }
main(2000); // { '5%': '91', '95%': '243' }

즉, (2000의 경우) 90% 신뢰구간은 (91, 243) 이다.

여기까지 공부했는데 약간 김 빠지긴 하지만 신뢰구간이 너무 넓어, 그닥 정확한 자료가 아님을 알 수 있다.

하긴 열차 3대만 목격했을 뿐인 상황에서 이정도면 꽤 놀라운 추정을 했다고는 볼 수 있겠다.

함께 읽기