이번에는 이전 회차들의 당첨 번호를 수집해, 각 번호가 등장할 확률을 계산한 뒤 그 확률에 따라 번호를 추출하는 기능을 구현해 보았다.
1. 데이터 수집
첫 번째 단계로, 이전 회차의 당첨 번호를 수집하고 정리했다. 로또 당첨 번호는 동행복권 홈페이지에서 간단히 다운받을 수 있다. 나는 600회차부터 1139회차까지의 당첨 번호를 다운받아 GPT를 이용해 분석했다.
이 분석을 통해 1번부터 45번까지 각 숫자가 나온 횟수와 그에 따른 확률을 구할 수 있었다. 이후 이 데이터는 DB에 저장했다.
2. 확률 데이터 저장
확률 값을 실시간으로 계산해 DB에 저장하려 했지만, 아직 일주일마다 웹 크롤링을 구현하지 못해 확률 자체를 미리 입력해 두었다. 추후 당첨 번호가 업데이트될 때마다 해당 번호의 등장 횟수를 증가시키고, 이를 바탕으로 확률 데이터를 업데이트할 예정이다.
3. 확률에 따른 번호 추출 함수 구현
확률에 따른 번호 추출 함수는 여러 자료를 참고해 최종적으로 다음과 같은 구조로 구현되었다. 컴포넌트에서 서버로부터 확률 데이터를 받아, 이 데이터를 바탕으로 번호를 추출하는 방식이다.
useEffect(() => {
fetch("/api/probabilityData")
.then((res) => res.json())
.then((data) => {
let tempArray = [];
for (let i = 1; i <= 45; i++) {
tempArray.push(data[0]["num" + i]);
}
setProbabilityData(tempArray);
}, []);
여기서 _id 값이 함께 넘어왔기 때문에, 숫자 값만을 가진 배열을 만들어 추출했다.
다음으로는 확률에 따른 번호 추출 함수를 소개한다. probabilityData는 1번부터 45번까지의 각 번호가 뽑힐 확률을 담고 있는 배열이다.
function probabilityPickFunction(probabilityData) {
const numbers = [];
const totalProbability = probabilityData.reduce((sum, prob) => sum + prob, 0);
while (numbers.length < 6) {
const ranNum = Math.random() * totalProbability;
let cumulativeProbability = 0;
for (let i = 0; i < probabilityData.length; i++) {
cumulativeProbability += probabilityData[i];
if (ranNum <= cumulativeProbability) {
const number = i + 1;
if (!numbers.includes(number)) {
numbers.push(number);
}
break;
}
}
}
return numbers.sort((a, b) => a - b);
}
4. 코드 설명
1. 번호를 담을 배열
const numbers = [];
6개의 숫자를 담을 배열을 선언한다.
2. 전체 확률 계산
const totalProbability = probabilityData.reduce((sum, prob) => sum + prob, 0);
각 숫자의 확률을 더해 전체 확률을 계산한다. 원래는 모든 확률을 합하면 100%가 되어야 하지만, 나는 DB에 확률을 소수점 3자리까지 저장했기 때문에 모든 확률을 더하면 99.995%가 된다. 이 때문에 totalProbability를 이용해 범위를 조금 조정했다.
3. 확률에 따른 번호 추출
while (numbers.length < 6) {
const ranNum = Math.random() * totalProbability;
let cumulativeProbability = 0;
for (let i = 0; i < probabilityData.length; i++) {
cumulativeProbability += probabilityData[i];
if (ranNum <= cumulativeProbability) {
const number = i + 1;
if (!numbers.includes(number)) {
numbers.push(number);
}
break;
}
}
}
이 부분이 핵심이다. 확률에 따라 번호를 뽑기 위해 누적 확률과 가중치 개념을 사용했다.
처음에 몰랐던 개념이기에 간단한 예시를 들어 설명 해보겠다. 1번이 뽑힐 확률이 10%, 2번이 20%, 3번이 70%라고 가정하면, 누적 확률은 아래와 같이 계산된다.
- 1번: 0 ~ 10%
- 2번: 10 ~ 30%
- 3번: 30 ~ 100%
그림으로 보면 이렇다.
이 상태에서 Math.random 을 통해 0부터 100% 사이의 아무 값이나 랜덤으로 뽑는 것이다.
25%라는 값이 랜덤으로 나왔다고 했을때, ranNum은 25가 되는 것이다.
그러면 위의 그림의 시작 부분을 0 끝 부분을 100 이라고 하면 25%는 가장 어두운 부분에 위치하므로 숫자 2번이 당첨되었다라고 할 수 있게 된다.
결론적으로 확률이 높은 숫자가 뽑힐 가능성이 더 크다는 것을 알 수 있게 되고, 이게 바로 가중치가 반영된 방식이다. 이제 내 코드로 넘어가보자.
while (numbers.length < 6) {
const ranNum = Math.random() * totalProbability;
let cumulativeProbability = 0;
for (let i = 0; i < probabilityData.length; i++) {
cumulativeProbability += probabilityData[i];
if (ranNum <= cumulativeProbability) {
const number = i + 1;
if (!numbers.includes(number)) {
numbers.push(number);
}
break;
}
}
}
위의 가중치와 누적확률의 개념에 따라 이 코드도 하나하나 살펴보자.
1. 무작위 숫자 추출
const ranNum = Math.random() * totalProbability;
우선 Math.random을 통해 무작위 값을 뽑아낸다. 단 위에서 말했던 것처럼. 나는 모든 확률의 합이 100%가 아닌 99.995%이므로 totalProbability 를 곱해서 범위를 수정해주었다.
2. 누적 확률 값 초기화
let cumulativeProbability = 0;
그리고 cumulativeProbability 값을 0으로 초기화 해주었다.
3. 무작위 숫자가 해당하는 확률 범위 까지 누적 확률 계산 하기
for (let i = 0; i < probabilityData.length; i++) {
cumulativeProbability += probabilityData[i];
if (ranNum <= cumulativeProbability) {
const number = i + 1;
if (!numbers.includes(number)) {
numbers.push(number);
}
break;
}
}
이제 각 숫자가 뽑힐 확률이 들어 있는 배열을 순회하면서 ranNum이 해당하는 범위 까지 누적 확률을 더해준다.
그리고 그 숫자가 범위에 속할 경우 중복 검사를 한 뒤 실제로 컴포넌트로 보낼 numbers 배열에 추가해주고 for 문을 탈출해 6개의 숫자가 나올때까지 반복한다.
i에 1을 더한 이유는 배열의 인덱스는 0부터 시작하지만 로또 번호는 1부터 시작하기 때문이다.
5. 중복 방지 및 최종 코드
위 코드를 만들고 나서, 중복 검사 부분에 오류가 있다는 것을 깨달았다. 로또 번호는 중복 없이 추출해야 하므로, 뽑힌 번호를 제외하고 다음 번호를 선택할 수 있게 코드를 수정했다.
최종 코드는 다음과 같다.
function probabilityPickFunction(probabilityData) {
const numbers = [];
const availableNumbers = Array.from(
{ length: probabilityData.length },
(_, i) => i + 1
);
const totalProbability = probabilityData.reduce((sum, prob) => sum + prob, 0);
while (numbers.length < 6) {
const ranNum = Math.random() * totalProbability;
let cumulativeProbability = 0;
for (let i = 0; i < availableNumbers.length; i++) {
cumulativeProbability += probabilityData[i];
if (ranNum <= cumulativeProbability) {
numbers.push(availableNumbers[i]);
availableNumbers.splice(i, 1);
break;
}
}
}
return numbers.sort((a, b) => a - b);
}
이게 최종적으로 안성된 코드이다. 대부분의 코드는 아까 코드와 비슷하고 몇가지 코드만 추가되었다.
1. 사용 가능한 번호 배열 생성
const availableNumbers = Array.from(
{ length: probabilityData.length },
(_, i) => i + 1
);
1번부터 45번까지 숫자를 가진 배열을 만들어준다.
2. 뽑힌 번호는 배열에서 제외
availableNumbers.splice(i, 1);
번호를 뽑으면 splice로 배열에서 제거해 중복을 방지한다.
이게 완성된 모습이다. 각 뽑기 방법에 따라 왼쪽에 종류도 표시되도록 했다.
다음 단계는 언더독 픽 기능을 구현하는 것이다.
혹시 이 글을 보시는 분들 중에서 제 코드에 대한 피드백이나 개선할 점이 있다면 언제든지 알려주시면 감사하겠습니다. 저는 아직 초보 개발자이지만(아니 개발자 지망생 이지만), 계속해서 배워 나가고 있습니다.
'나만의 로또 번호 추첨 사이트' 카테고리의 다른 글
#5 : 언더독 뽑기 기능 구현하기 (1) | 2024.10.09 |
---|---|
#3 : 랜덤 뽑기 기능 만들기 (Fisher-Yates 알고리즘) (5) | 2024.10.03 |
#2 : 기본적인 폴더 생성 및 메인 페이지 틀 제작 (2) | 2024.09.24 |
#1 : 생에 첫 프로젝트 : 로또 번호 랜덤 추출 사이트 (1) | 2024.09.19 |