rand 로 결과가 정해지는 기능을 만들었다. 설정한 확률대로 나와야 하는데 테스트를 몇 번 돌려 보니 대충 맞는 것 같았다. 대충 맞는다로는 안 되는 종류였다.
Table of contents
Open Table of contents
몇 번으로는 모른다
확률 5%짜리를 20번 돌리면 한 번쯤 나오고 안 나올 수도 있다. 적은 횟수로는 판단이 안 되고 우연히 맞을 수도 우연히 틀릴 수도 있다.
그래서 PHP 스크립트로 많이 돌리는 도구를 따로 만들었다.
100만 회 실행 → 실제 비율 집계 → 설정값과 비교
CLI 로 돌리면 몇 초면 끝나고 눈에 띄게 틀린 것은 바로 나온다. srand 에 고정 씨앗을 줘서 같은 수열을 다시 돌릴 수 있게 해 두면 이상한 결과가 나왔을 때 그 자리를 다시 볼 수 있다.
실제로 틀린 것이 나왔다
돌려 보니 한 항목이 설정보다 적게 나왔고 원인은 rand 결과의 경계 처리였다.
$r = rand(0, 99);
if ($r < 5) return 'A'; // 0~4 → 5%
else if ($r < 10) return 'B'; // 5~9 → 5%
...
else if ($r < 100) return 'Z'; // 마지막
누적 구간을 손으로 계산해 넣었는데 한 곳이 어긋나 있었다. 숫자로 보면 안 보이는데 100만 번 돌려서 비율을 세니 명확했다.
계산이 틀려도 오류가 안 나고 비율만 조금 어긋난다. 많이 돌리지 않았으면 이 차이는 안 보였을 것이다.
구조를 바꿨다
if 사슬에서 경계를 손으로 계산하는 것이 문제였다. 항목을 추가하면 뒤를 다 고쳐야 한다.
$weights = ['A' => 5, 'B' => 5, 'C' => 20, ... ];
$total = array_sum($weights);
$r = mt_rand(1, $total);
foreach ($weights as $key => $w) {
if ($r <= $w) return $key;
$r -= $w;
}
가중치만 정의하고 경계는 array_sum 으로 계산하게 바꿨다. 항목을 추가해도 다른 데를 안 고치고 합이 100이 아니어도 된다.
사람이 계산할 자리를 없애는 것이 이런 종류의 오차를 막는 방법이었다.
기대값 말고 분산
비율이 맞는 것을 확인하고 나서 하나를 더 봤다. 연속으로 안 나오는 구간이 얼마나 긴가다.
5% 확률이면 평균 20번에 한 번인데 운이 나쁘면 100번 넘게 안 나올 수 있어서 mt_rand 수열에서 그 분포를 봤다.
최대 연속 미발생: 137회
이것이 사용자 경험을 정한다. 평균이 맞아도 137번을 겪은 사람은 확률 조작이 아니냐고 생각한다. 기대값만 보면 안 보이는 부분이다.
일정 횟수 이상 안 나오면 weights 값을 점점 올리는 보정도 검토했다.
보정 전: 평균 20.0회, 최대 137회
보정 후: 평균 19.4회, 최대 68회
평균은 3% 정도 움직이고 최악이 절반으로 줄었다. 이 득실을 숫자로 보여 주니 결정이 빨랐고 느낌상 나은 것 같다가 아니라 얼마나 나은지가 나온다.
검증 — 난수 생성기와 도구 보존
기본 난수 함수가 무엇을 쓰는지도 확인했다. rand 는 판에 따라 품질이 낮아서 주기가 짧거나 하위 비트에 규칙이 있다.
그래서 mt_rand 로 바꿨다. 범위를 직접 받는 형태를 쓰면 나머지 연산으로 자를 때 생기는 치우침도 없어지는데 rand() 를 10000으로 나눈 나머지로 만분율을 만들면 범위가 나누어떨어지지 않는 만큼 앞쪽 값이 한 번씩 더 나온다.
계산이 맞아도 난수 쪽에서 치우칠 수 있다는 것이 여기서 나온다. 바꾸고 나서 시뮬레이션을 mt_rand 로 다시 돌려 결과가 같은지 봤고 같았으니 바꿔도 안전하다는 확인이 됐다.
이 시뮬레이터를 버리지 않고 남겼다. 설정을 바꿀 때마다 돌려서 그 변경이 의도한 확률을 만드는지 배포 전에 확인한다.
몇 번 유용했는데 weights 에 항목을 추가하면서 총합이 안 맞은 것을 잡았다. 한 번 쓰고 버릴 도구가 아니었다.
정리
- 확률 로직은 몇 번 돌려서는 판단이 안 된다
- 많이 돌려서 분포를 보는 도구를 만든다
- 고정 씨앗을 주면 같은 수열을 다시 볼 수 있다
- 경계를 손으로 계산하지 말고
array_sum으로 가중치에서 유도한다 - 계산이 틀려도 오류가 안 나고 비율만 어긋난다
- 기대값이 맞아도 분산이 다르면 겪는 것이 달라진다
- 연속 미발생 최대 길이를 시뮬레이션으로 재 본다
- 보정의 득실을 숫자로 보여 주면 결정이 빠르다
- 나머지로 자르면 범위가 나누어떨어지지 않는 만큼 앞쪽이 자주 나온다
- 계산이 맞아도 난수 쪽에서 치우칠 수 있다
rand를mt_rand로 바꾼 뒤 결과가 같은지 본다- 도구를 남겨 설정을 바꿀 때마다 돌린다