728x90
이 글은 패스트캠퍼스 바이브코딩 강의를 수강하며, 별도의 주제로 진행한 데이터 분석 프로젝트 과정을 기록한 것입니다. 코딩과 글 작성에는 클로드코드와 커서AI 및 퍼플렛시티를 함께 활용했음을 미리 밝힙니다.

Episode 4: 기계가 배우는 운의 법칙 (The Machine's Fortune: Learning Rules)

머신러닝으로 번호 추천하기: 점수 시스템과 확률 모델 설계

숫자 하나하나에 점수를 매기기 시작했다. 14번 66.1점, 17번 66.1점. 기계가 605회의 패턴을 학습했다.

PythonNumPyscikit--learn

작성일: 2026-01-10. 난이도: ⭐⭐⭐⭐☆. 예상 소요 시간: 5-6시간 버전: v3.0


📖 들어가며

시계열 분석을 마치고, 핫넘버와 콜드넘버를 발견했다.

"이제 어떻게 번호를 추천할까?"

무작위로? 아니면 가장 많이 나온 번호만? 최근 핫넘버만?

고민 끝에 내린 결론:
"기계에게 배우게 하자."

머신러닝(Machine Learning)이라는 단어가 떠올랐다. 하지만 로또는 독립 시행이다. 과거가 미래를 예측할 수 없다. 그럼에도 불구하고...

"605회의 데이터가 말하는 패턴은 무엇일까?"

분류(Classification)도 아니고, 회귀(Regression)도 아니다. 로또는 당첨/미당첨을 예측하는 문제가 아니라, "어떤 번호를 선택할지"의 문제다.

답은 점수 시스템(Scoring System)이었다.


🤔 왜 점수 기반 시스템인가?

머신러닝 접근법 비교

1. 분류(Classification) 접근법

# 각 번호를 "당첨" vs "미당첨"으로 분류?
# ❌ 문제점: 매 회차 정확히 6개만 당첨되므로 의미 없음

2. 회귀(Regression) 접근법

# 각 번호의 "당첨 확률"을 예측?
# ❌ 문제점: 독립 시행이므로 확률은 항상 1/45

3. 점수 시스템(Scoring System) 접근법

# 각 번호의 "선택 가치"를 점수로 환산
# ✅ 장점: 여러 특징(Feature)을 종합하여 랭킹 가능
# ✅ 장점: 해석 가능성(Interpretability) 높음

선택: 점수 시스템!


🔬 특징 추출 (Feature Engineering)

"각 번호의 특징(Feature)을 정의하라"

45개 번호 각각에 대해 8가지 특징을 추출했다.

def extract_number_features(self):
    """번호별 특징 추출"""
    print("📊 번호별 특징 추출 중...")

    for num in range(1, 46):
        # 1. 전체 출현 빈도 (Total Frequency)
        total_frequency = 0

        # 2. 최근 50회 출현 빈도 (Recent 50 Frequency)
        recent_50_frequency = 0

        # 3. 최근 100회 출현 빈도 (Recent 100 Frequency)
        recent_100_frequency = 0

        # 4. 마지막 출현 이후 경과 회차 (Absence Length)
        last_appearance = -1
        absence_length = 0

        # 5. 평균 출현 간격 (Average Interval)
        appearances = []
        intervals = []
        avg_interval = 0

        # 6. 출현 간격 표준편차 (Interval Std)
        std_interval = 0

        # 7. 구간 (Section: Low 1-15, Mid 16-30, High 31-45)
        if 1 <= num <= 15:
            section = 'low'
        elif 16 <= num <= 30:
            section = 'mid'
        else:
            section = 'high'

        # 8. 홀짝 (Odd/Even)
        odd_even = 'odd' if num % 2 == 1 else 'even'

        # 계산 로직 (생략)
        # ...

        self.number_features[num] = {
            'total_frequency': total_frequency,
            'recent_50_frequency': recent_50_frequency,
            'recent_100_frequency': recent_100_frequency,
            'absence_length': absence_length,
            'avg_interval': avg_interval,
            'std_interval': std_interval,
            'section': section,
            'odd_even': odd_even,
            'hotness_score': recent_50_frequency / 10  # 핫넘버 점수
        }

    print(f"✓ 45개 번호에 대한 특징 추출 완료")

핵심 특징(Feature):

  • 빈도(Frequency): 자주 나온 번호일수록 높은 점수
  • 트렌드(Trend): 최근 상승세인 번호일수록 높은 점수
  • 부재 기간(Absence): 오래 안 나온 번호일수록 높은 점수
  • 핫넘버(Hotness): 최근 50회 출현 횟수 기반 점수

🎯 종합 점수 계산

"4가지 점수 구성요소(Score Components)"

각 번호는 최대 100점을 받을 수 있다.

def calculate_number_scores(self):
    """각 번호의 종합 점수 계산"""
    print("\n🎯 번호별 종합 점수 계산 중...")

    scores = {}

    for num in range(1, 46):
        features = self.number_features[num]

        # 1. 빈도 점수 (Frequency Score: 0-30점)
        freq_score = min(features['total_frequency'] / 100 * 30, 30)

        # 2. 트렌드 점수 (Trend Score: 0-30점)
        trend_score = features['recent_50_frequency'] / 50 * 30

        # 3. 부재 기간 점수 (Absence Score: 0-20점)
        absence_score = min(features['absence_length'] / 20 * 20, 20)

        # 4. 핫넘버 점수 (Hotness Score: 0-20점)
        hotness_score = min(features['hotness_score'] / 10 * 20, 20)

        # 총점 계산
        total_score = freq_score + trend_score + absence_score + hotness_score

        scores[num] = {
            'total_score': total_score,
            'freq_score': freq_score,
            'trend_score': trend_score,
            'absence_score': absence_score,
            'hotness_score': hotness_score,
            'features': features
        }

    self.number_scores = scores
    return scores

점수 가중치(Score Weights) 설계:

구성요소 최대 점수 비율 의미
빈도 점수(Frequency) 30점 30% 전체 출현 횟수
트렌드 점수(Trend) 30점 30% 최근 50회 출현
부재 점수(Absence) 20점 20% 미출현 기간
핫넘버 점수(Hotness) 20점 20% 최근 핫넘버 여부
총점 100점 100% -

결과:

상위 10개 번호:
  1. 번호 14: 66.1점
  2. 번호 17: 66.1점
  3. 번호 42: 66.1점
  4. 번호 11: 62.5점
  5. 번호 19: 62.0점
  6. 번호 13: 61.9점
  7. 번호 22: 59.5점
  8. 번호 38: 58.8점
  9. 번호 15: 58.1점
  10. 번호 45: 58.1점


▲ 상위 20개 번호의 종합 점수(Total Score) - 최대 100점

인사이트:

  • 14번, 17번, 42번이 공동 1위 (66.1점)
  • 최저 점수는 약 35점
  • 점수 분포가 비교적 고르게 퍼짐

📊 점수 구성요소 분해

"각 번호의 강점은 무엇인가?"


▲ 상위 10개 번호의 점수 구성요소(Score Components) 분해 - 누적 막대 그래프(Stacked Bar Chart)

14번 분석:

  • 빈도 점수(Frequency): 28.5점 (높음 ✅)
  • 트렌드 점수(Trend): 18.0점 (보통)
  • 부재 점수(Absence): 0점 (최근 출현 ❌)
  • 핫넘버 점수(Hotness): 19.6점 (높음 ✅)
  • 총점: 66.1점

42번 분석:

  • 빈도 점수(Frequency): 20.1점 (보통)
  • 트렌드 점수(Trend): 12.0점 (보통)
  • 부재 점수(Absence): 20.0점 (최대! ✅)
  • 핫넘버 점수(Hotness): 14.0점 (보통)
  • 총점: 66.1점

핵심 발견:

  • 같은 총점이어도 구성요소가 다름
  • 14번: 빈도 + 핫넘버 강점
  • 42번: 부재 기간 강점 (오래 안 나옴)

🎨 패턴 학습 (Pattern Learning)

"과거 605회의 패턴을 학습하라"

점수만으로는 부족하다. 조합의 패턴도 학습해야 한다.

1. 구간 분포 패턴(Section Pattern Distribution)

def analyze_section_patterns(self):
    """구간 분포 패턴 학습"""
    print("📊 구간 패턴 학습 중...")

    section_patterns = {'distribution': []}

    for _, row in self.loader.numbers_df.iterrows():
        nums = row['당첨번호']

        # 저구간(Low: 1-15), 중구간(Mid: 16-30), 고구간(High: 31-45) 개수
        low = sum(1 for n in nums if 1 <= n <= 15)
        mid = sum(1 for n in nums if 16 <= n <= 30)
        high = sum(1 for n in nums if 31 <= n <= 45)

        section_patterns['distribution'].append((low, mid, high))

    # 가장 흔한 구간 분포 (Most Common Section Pattern)
    from collections import Counter
    dist_counter = Counter(section_patterns['distribution'])
    section_patterns['most_common'] = dist_counter.most_common(10)

    print(f"✓ 가장 흔한 구간 분포: {section_patterns['most_common'][0]}")
    return section_patterns

결과:

✓ 가장 흔한 구간 분포: ((2, 2, 2), 87)


▲ 구간 패턴 분포(Section Pattern Distribution) - TOP 10

TOP 10 구간 패턴:

순위 패턴 (Low-Mid-High) 출현 횟수 비율
🥇 2-2-2 87회 14.4%
🥈 3-1-2 61회 10.1%
🥉 2-3-1 54회 8.9%
4 2-1-3 50회 8.3%
5 1-3-2 47회 7.8%
6 1-2-3 46회 7.6%
7 3-2-1 43회 7.1%
8 3-3-0 29회 4.8%
9 0-3-3 27회 4.5%
10 2-2-2 24회 4.0%

인사이트:

  • 2-2-2 패턴이 압도적 (14.4%, 87회)
  • 균형잡힌 분포가 가장 흔함
  • 한쪽으로 치우친 패턴(예: 0-3-3)은 드물음

2. 홀짝 분포 패턴(Odd/Even Pattern Distribution)

def analyze_odd_even_patterns(self):
    """홀짝 분포 패턴 학습"""
    print("📊 홀짝 패턴 학습 중...")

    odd_even_patterns = {'distribution': []}

    for _, row in self.loader.numbers_df.iterrows():
        nums = row['당첨번호']

        # 홀수(Odd), 짝수(Even) 개수
        odd = sum(1 for n in nums if n % 2 == 1)
        even = 6 - odd

        odd_even_patterns['distribution'].append((odd, even))

    # 가장 흔한 홀짝 분포
    from collections import Counter
    oe_counter = Counter(odd_even_patterns['distribution'])
    odd_even_patterns['most_common'] = oe_counter.most_common()

    print(f"✓ 가장 흔한 홀짝 분포: {odd_even_patterns['most_common'][0]}")
    return odd_even_patterns

결과:

✓ 가장 흔한 홀짝 분포: ((3, 3), 197)


▲ 홀짝 분포 패턴(Odd/Even Distribution) - TOP 8

TOP 8 홀짝 패턴:

순위 패턴 (Odd:Even) 출현 횟수 비율
🥇 3 Odd : 3 Even 197회 32.6% ⭐
🥈 4 Odd : 2 Even 148회 24.5%
🥉 2 Odd : 4 Even 147회 24.3%
4 5 Odd : 1 Even 57회 9.4%
5 1 Odd : 5 Even 48회 7.9%
6 6 Odd : 0 Even 5회 0.8%
7 0 Odd : 6 Even 3회 0.5%

핵심 발견:

  • 3:3 패턴이 압도적 (32.6%, 197회)
  • 4:2와 2:4도 각각 24% 이상
  • 홀0:짝6 또는 홀6:짝0은 극히 드물음 (< 1%)

3. 합계 패턴(Sum Pattern)

def analyze_sum_patterns(self):
    """합계 패턴 학습"""
    print("📊 합계 패턴 학습 중...")

    sums = []
    for _, row in self.loader.numbers_df.iterrows():
        nums = row['당첨번호']
        total = sum(nums)
        sums.append(total)

    sum_patterns = {
        'mean': np.mean(sums),  # 평균(Mean)
        'median': np.median(sums),  # 중앙값(Median)
        'std': np.std(sums),  # 표준편차(Std)
        'min': np.min(sums),  # 최소(Min)
        'max': np.max(sums),  # 최대(Max)
        'q1': np.percentile(sums, 25),  # 1사분위수(Q1)
        'q3': np.percentile(sums, 75)   # 3사분위수(Q3)
    }

    print(f"✓ 합계 평균(Mean): {sum_patterns['mean']:.1f}, 표준편차(Std): {sum_patterns['std']:.1f}")
    return sum_patterns

결과:

✓ 합계 평균(Mean): 138.1, 표준편차(Std): 30.8

평균(Mean): 138.1
중앙값(Median): 137.0
표준편차(Std): 30.8
최소(Min): 46
최대(Max): 252
1사분위수(Q1): 116.0
3사분위수(Q3): 159.0

해석:

  • 평균 합계: 138.1 (약 6개 평균: 23)
  • 95% 구간: 평균 ± 2×표준편차 = 76.5 ~ 199.7
  • 추천 시 합계를 이 범위 내로 제한 가능

🎲 확률 가중치 생성

"점수를 확률로 변환하라"

점수를 그대로 사용할 수도 있지만, 확률 분포(Probability Distribution)로 변환하면 더 다양한 조합을 생성할 수 있다.

def get_probability_weights(self):
    """점수 기반 확률 가중치 생성"""
    weights = {}

    # 점수의 제곱을 가중치로 사용 (강조 효과)
    for num, score_info in self.number_scores.items():
        score = score_info['total_score']
        weights[num] = score ** 2

    # 정규화 (Normalization)
    total = sum(weights.values())
    for num in weights:
        weights[num] /= total

    return weights

왜 제곱?

  • 선형: 66점 vs 60점 → 가중치 비율 1.1배
  • 제곱: 66점 vs 60점 → 가중치 비율 1.21배
  • 상위 번호를 더 강조하는 효과


▲ 번호별 확률 가중치(Probability Weights) 분포 - 골드: 상위 10개 번호

확률 가중치 활용:

import numpy as np

# 점수 기반 가중치 추출
weights = model.get_probability_weights()

# 확률 가중치로 번호 샘플링
numbers = list(range(1, 46))
probs = [weights.get(n, 0) for n in numbers]

# 6개 번호 선택 (중복 없이)
selected = np.random.choice(numbers, size=6, replace=False, p=probs)
print(f"추천 번호: {sorted(selected)}")

예시 출력:

추천 번호: [11, 14, 17, 19, 27, 38]

💡 배운 점과 인사이트

1. Feature Engineering의 중요성

✅ 좋은 특징(Feature)의 조건:

# 1. 측정 가능성 (Measurable)
total_frequency = count_appearances(num)

# 2. 의미 있는 정보 (Informative)
trend_score = recent_frequency - overall_frequency

# 3. 독립성 (Independent)
# 빈도와 트렌드는 서로 다른 정보 제공

# 4. 정규화 가능 (Normalizable)
normalized_score = (value - min) / (max - min) * max_points

2. 점수 시스템 설계

✅ 가중치 설계 원칙:

# 가중치 = 중요도
weights = {
    'frequency': 0.30,  # 30% - 전체 빈도
    'trend': 0.30,      # 30% - 최근 트렌드
    'absence': 0.20,    # 20% - 미출현 기간
    'hotness': 0.20     # 20% - 핫넘버 점수
}

# 총합 = 100%
assert sum(weights.values()) == 1.0

✅ 점수 정규화:

# 각 구성요소를 0-max_points 범위로 정규화
freq_score = min(total_frequency / 100 * 30, 30)
# min() 함수로 상한선 설정

# 또는 sigmoid 함수 사용
import math
sigmoid_score = max_points / (1 + math.exp(-normalized_value))

3. 패턴 학습

✅ Counter 활용:

from collections import Counter

# 패턴 빈도 계산
patterns = [(2,2,2), (3,1,2), (2,2,2), (2,3,1), ...]
pattern_counter = Counter(patterns)

# 상위 N개 패턴
top_patterns = pattern_counter.most_common(10)
# [((2, 2, 2), 87), ((3, 1, 2), 61), ...]

✅ NumPy 통계:

import numpy as np

# 기본 통계
mean = np.mean(data)
median = np.median(data)
std = np.std(data)

# 백분위수 (Percentile)
q1 = np.percentile(data, 25)  # 1사분위수
q3 = np.percentile(data, 75)  # 3사분위수
iqr = q3 - q1  # 사분위 범위 (Interquartile Range)

4. 확률 분포 생성

✅ 가중치 정규화:

# 원시 가중치
raw_weights = {1: 100, 2: 80, 3: 60, ...}

# 정규화 (합계 = 1.0)
total = sum(raw_weights.values())
normalized = {k: v/total for k, v in raw_weights.items()}

# 검증
assert abs(sum(normalized.values()) - 1.0) < 1e-10

✅ NumPy 가중치 샘플링:

import numpy as np

numbers = [1, 2, 3, 4, 5]
weights = [0.1, 0.2, 0.3, 0.25, 0.15]  # 합계 = 1.0

# 가중치 기반 샘플링 (중복 없이)
selected = np.random.choice(numbers, size=3, replace=False, p=weights)
# [3, 2, 5] (확률에 따라 다름)

📊 네 번째 마일스톤 달성

v3.0 작업 완료:

특징 추출(Feature Engineering) (45개 번호, 8가지 특징)
점수 시스템 설계 (4가지 구성요소, 최대 100점)
패턴 학습 (구간/홀짝/합계 분포)
확률 가중치 생성 (점수 기반 정규화)
상위 20개 번호 도출 (14, 17, 42번 공동 1위)

흥미로운 발견

  1. 14번, 17번, 42번이 공동 1위 (각 66.1점)
  2. 같은 점수, 다른 강점 (빈도 vs 부재 기간)
  3. 2-2-2 구간 패턴이 압도적 (14.4%, 87회)
  4. 3:3 홀짝 패턴이 최다 (32.6%, 197회)
  5. 합계 평균 138.1 (표준편차 30.8)

통계적 의미

점수 분포:

  • 최고 점수: 66.1점 (3개 번호)
  • 최저 점수: 약 35점
  • 평균 점수: 약 48점
  • 점수 차이가 명확함

패턴의 일관성:

  • 구간 2-2-2: 14.4% (무작위 예상: ~10%)
  • 홀짝 3:3: 32.6% (무작위 예상: ~31%)
  • 홀짝은 거의 무작위와 동일, 구간은 약간 편향

🚀 다음 에피소드 예고

5편: "일곱 가지 선택의 기로" - 7가지 번호 추천 전략 구현

다음 편에서는:

  • 점수 기반 추천 전략
  • 확률 가중치 추천 전략
  • 패턴 기반 추천 전략
  • 그리드 패턴 추천 전략
  • 연속 번호 포함 전략
  • 무작위 추천 (대조군)
  • ⭐ 하이브리드 추천 (모든 전략 통합)

미리보기:

# recommendation_system.py
class LottoRecommendationSystem:
    def generate_hybrid(self, n_combinations=5):
        """하이브리드 추천 (최고 품질)"""
        # 4가지 전략 결합
        score_based = self.generate_by_score(n_combinations)
        prob_based = self.generate_by_probability(n_combinations)
        pattern_based = self.generate_by_pattern(n_combinations)
        grid_based = self.generate_grid_based(n_combinations)

        # 통합 및 재점수 계산
        all_combinations = score_based + prob_based + pattern_based + grid_based
        unique_combinations = list(set(map(tuple, all_combinations)))

        # 최고 점수 N개 선택
        scored = [(combo, self.calculate_combo_score(combo))
                  for combo in unique_combinations]
        scored.sort(key=lambda x: x[1], reverse=True)

        return [list(combo) for combo, _ in scored[:n_combinations]]

🔗 관련 링크


💬 마무리하며

"기계가 배우기 시작했다."

605회의 데이터를 학습하고, 45개 번호에 점수를 매겼다. 14번 66.1점, 17번 66.1점, 42번 66.1점.

점수 구성요소를 분해하니 각 번호의 강점이 보였다. 14번은 빈도와 핫넘버로, 42번은 부재 기간으로 같은 점수를 받았다.

패턴 학습은 의외의 결과를 주었다. 2-2-2 구간 패턴이 14.4%로 압도적이었고, 3:3 홀짝 패턴은 32.6%로 무작위와 비슷했다.

확률 가중치로 변환하니 다양한 조합이 가능해졌다. 제곱을 사용해 상위 번호를 강조했다.

이제 이 점수와 패턴을 활용해 실제로 번호를 추천해야 한다. 7가지 전략을 설계하고, 하이브리드로 통합할 차례다.

기계가 학습한 운의 법칙, 과연 작동할까?


📌 SEO 태그

#포함 해시태그

#머신러닝 #점수시스템 #특징추출 #FeatureEngineering #확률가중치 #패턴학습 #NumPy #Counter #정규화 #가중치샘플링

쉼표 구분 태그

머신러닝, 점수시스템, 특징추출, 빈도점수, 트렌드점수, 부재점수, 핫넘버, 구간패턴, 홀짝패턴, 확률가중치, 정규화, 가중치샘플링


작성: @MyJYP
시리즈: 로또 645 데이터 분석 프로젝트 (4/10)
라이선스: CC BY-NC-SA 4.0


📊 Claude Code 사용량

작업 전:

  • 세션 사용량: 80,891 tokens (0%)

작업 후:

  • 세션 사용량: 98,036 tokens (17%사용)

사용량 차이:

  • Episode 4 작성 사용량: 17,145 tokens (약 17K tokens)
  • 이미지 5개 생성 + 본문 작성 포함
  • 주간 2% (67%-65%)
728x90
Posted by 댕기사랑
,