11 June, 2022

자바스크립트 확률에 따라 추첨하기 Extract an item according to percentage

Random extraction
Photo by dylan nolte on Unsplash.
tweet
share

Please support me with,

Table Of Contents
# # # #

로또번호추첨기를 만들며 사용하게 된 확률에 따른 항목 추출에 대한 포스팅이다.

Math.random()

자바스크립트에는 Math.random() 이라는 메서드가 있다. 이 메서드를 이용하면 0 이상 1 미만의 임의 난수를 생성할 수 있다.

단순하게 배열에서 랜덤으로 항목을 추출하는 방법은 아래와 같이 간단하게 만들 수 있다.

/**
 * Get random index from an array.
 * @param array - An array to get index.
 * @returns {number} A random index between `0` to `array.length`.
 */
function getRandomIndexFromArray(array) {
  return Math.floor(Math.random() * array.length);
}

const fruits = ['Apple', 'Banana', 'Orange', 'Grape'];

// This will return different result between `0` to `fruits.length`.
console.log(getRandomIndexFromArray(fruits));

하지만 이 경우는 사실 모든 항목이 같은 확률로 추첨되는 것이나 마찬가지다.

그렇다면 만약 결과값의 횟수가 사과 > 바나나 > 오렌지 > 포도 순서가 되게 하려면 어떻게 하면 될까?

항목 수에 따른 통제

간단한 편법으로는, 배열에 더 많이 추출하고 싶은 항목을 더 많이 넣으면 된다.

// 4 Apples, 3 Bananas, 2 Oranges, 1 Grape.
// The `getRandomIndexFromArray()` method will pick
// `Apple` more than `Banana`, `Banana` more than `Orange`, `Orange` more than `Grape`.
const fruits = [
  'Apple',
  'Apple',
  'Apple',
  'Apple',
  'Banana',
  'Banana',
  'Banana',
  'Orange',
  'Orange',
  'Grape',
];

무식한 방법이지만 아주 간단하게 해결할 수 있다.

만약 사과를 20%의 확률, 바나나를 40%의 확률, 오렌지를 5%의 확률, 포도를 35%의 확률로 추출하고 싶다면 사과 20개, 바나나 40개, 오렌지 5개, 포도 35개를 배열에 때려넣으면 된다.

하지만 이는 분명한 메모리 낭비고 만약 소수점 단위의 확률까지 다루게 된다면 수 천, 수 만 개의 항목을 배열에 때려넣어야 한다.

항목 별 확률에 따른 통제

로또 번호는 다들 알다시피 45개가 존재하고, 내가 만든 사이트는 기존 당첨 내역을 통계삼아 각 번호별로 적절한 확률을 부여해 추출하기 때문에 위와 같은 방법은 적절하지 않았다.

그리하여 생각해낸 방법이 각 항목별로 확률을 입력하고, 그 확률에 따라 번호를 추출하는 것이었다.

/**
 * Get random index from an array with percentages.
 * @param array - An array to get index.
 * @param percentages - An array of percentage for each item in array. The total of every items should be `1`.
 * @returns {number} A random index between `0` to `array.length`.
 */
function getRandomIndexFromArrayByPercentage(array, percentages) {
  const transformedPercentages = [];
  
  percentages.forEach((percentage, index) => {
    const previousPercentage = transformedPercentages[index - 1] || 0;
    
    transformedPercentages.push(previousPercentage + percentage);
  });
  
  const random = Math.random();
  
  return transformedPercentages.findIndex((percentage, index) => {
    const previousPercentage = transformedPercentages[index - 1] || 0;
    
    return random >= previousPercentage && random < percentage;
  });
}

const fruits = ['Apple', 'Banana', 'Orange', 'Grape'];
const percentages = [0.2, 0.4, 0.05, 0.35];

console.log(getRandomIndexFromArrayByPercentage(fruits, percentages));

위의 getRandomIndexFromArrayByPercentage() 함수는 첫 번째 파라미터로 인덱스 값을 추출할 배열을 받고, 두 번째 파라미터로 각 배열 항목에 대한 확률 값을 담고있는 배열을 받는다. percentages 배열의 모든 값을 더하면 1이 되어야 한다.

const transformedPercentages = [];

percentages.forEach((percentage, index) => {
  const previousPercentage = transformedPercentages[index - 1] || 0;
  
  transformedPercentages.push(previousPercentage + percentage);
});

함수 내에서 맨 처음으로 하는 일은 percentagesforEach() 메서드로 루프하면서 transformedPercentages에 값을 저장하는 것이다. 이 때 forEach() 콜백 함수에서는 현재 인덱스 이전의 인덱스를 이용해 previousPercentage를 가져오고 이 값을 현재 인덱스의 percentage와 더하여 transformedPercentages에 저장하는데, 위에서 봤던 [0.2, 0.4, 0.05, 0.35] 값은 [0.2, 0.6000000000000001, 0.6500000000000001, 1]로 저장된다.

만약 현재 인덱스가 0이라면 이전 값이 없기 때문에 || 기호를 이용해 previousPercentage의 기본값을 0으로 지정해준다.

const random = Math.random();

return transformedPercentages.findIndex((percentage, index) => {
  const previousPercentage = transformedPercentages[index - 1] || 0;
  
  return random >= previousPercentage && random < percentage;
});

그 다음은 Math.random() 메서드를 이용해 난수를 생성하고 transformedPercentages 배열에서 findIndex() 메서드를 이용해 해당 난수가 포함되는 범위의 인덱스를 찾아내는 것이다.

findIndex() 콜백 함수에서는 난수가 transformedPercentages의 이전 인덱스 값과 현재 인덱스 값 사이에 있는지 확인하고 만약 그렇다면 true를 리턴하여 인덱스 값을 얻어내도록 한다.

이때도 역시 현재 인덱스가 0일 때 이전 값이 없기 때문에 previousPercentage에 기본값 0을 설정해줘야 한다.

마무리

확률에 따라 배열에서 항목을 추출하는 함수를 간단하게 구현해보았다. 그런데 중간에 보면 [0.2, 0.4, 0.05, 0.35] 값이 [0.2, 0.6000000000000001, 0.6500000000000001, 1]로 변환된다고 한 부분이 있는데, 0.6과 0.65에 붙은 0.0000000000000001은 오타가 아니다. 자바스크립트는 소수점 계산이 정확하지 않기 때문에 발생하는 오차인데, 이 정도는 별 의미가 없기 때문에 너그러이 넘어가도록 하자.

그리고 로또번호추첨기는 추가적인 기능 업데이트를 앞두고 있으니 우리 모두 부자가 되기 위해 자주 방문해보자.

Tags
Copyright 2022, tk2rush90, All rights reserved