로또 프리미엄 Phase 1 — 추천 성과 통계 + 회차 공략 리포트 API
- GET /api/lotto/stats/performance: 채점 이력 기반 성과 통계
(평균 일치 수, 등수 분포, 무작위 대비 개선율)
- GET /api/lotto/report/latest: 다음 회차 공략 리포트 자동 생성
- GET /api/lotto/report/{drw_no}: 특정 회차 공략 리포트
(과출현/냉각/오버듀 번호, 최근 패턴, 3가지 전략 추천, 신뢰도 점수)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,7 @@
|
||||
|
||||
import math
|
||||
from collections import Counter, defaultdict
|
||||
from datetime import datetime, timezone
|
||||
from typing import List, Tuple, Dict, Any, Optional
|
||||
|
||||
# 구간 정의: (시작, 끝) 포함
|
||||
@@ -352,3 +353,107 @@ def get_statistical_report(draws: List[Tuple[int, List[int]]]) -> Dict[str, Any]
|
||||
"overdue_numbers": [x["number"] for x in sorted_by_gap[:10]],
|
||||
"sum_distribution": sum_buckets,
|
||||
}
|
||||
|
||||
|
||||
def generate_weekly_report(draws: List[Tuple[int, List[int]]], target_drw_no: int) -> Dict[str, Any]:
|
||||
"""
|
||||
특정 회차 공략 리포트 생성.
|
||||
target_drw_no: 공략 대상 회차 (아직 추첨 안 된 회차)
|
||||
draws: target_drw_no 이전까지의 당첨번호 (오름차순)
|
||||
"""
|
||||
if not draws:
|
||||
return {"error": "데이터 없음"}
|
||||
|
||||
cache = build_analysis_cache(draws)
|
||||
total_draws = cache["total_draws"]
|
||||
freq_all = cache["freq_all"]
|
||||
last_seen_gap = cache["last_seen_gap"]
|
||||
|
||||
recent_10 = draws[-10:] if len(draws) >= 10 else draws
|
||||
recent_3 = draws[-3:] if len(draws) >= 3 else draws
|
||||
|
||||
# 과출현: 최근 10회에 2회 이상 출현 번호 (출현 많은 순)
|
||||
r10_nums = [n for _, nums in recent_10 for n in nums]
|
||||
r10_freq = Counter(r10_nums)
|
||||
hot_numbers = [n for n, _ in sorted(r10_freq.items(), key=lambda x: -x[1]) if r10_freq[n] >= 2]
|
||||
|
||||
# 냉각: 역대 출현 빈도 낮은 번호
|
||||
cold_numbers = sorted(range(1, 46), key=lambda n: freq_all.get(n, 0))[:10]
|
||||
|
||||
# 오버듀: 가장 오래 미출현 번호
|
||||
overdue_numbers = sorted(range(1, 46), key=lambda n: -last_seen_gap.get(n, 0))[:10]
|
||||
|
||||
# 최근 3회 연속 출현 (2회 이상)
|
||||
r3_nums = [n for _, nums in recent_3 for n in nums]
|
||||
r3_freq = Counter(r3_nums)
|
||||
triple_appear = sorted(n for n, cnt in r3_freq.items() if cnt >= 2)
|
||||
|
||||
recent_sums = [sum(nums) for _, nums in recent_10]
|
||||
recent_odd = [sum(1 for n in nums if n % 2 == 1) for _, nums in recent_10]
|
||||
|
||||
# 갭 기반 가중치 (오래된 번호일수록 높음)
|
||||
gap_w = {n: last_seen_gap.get(n, 0) for n in range(1, 46)}
|
||||
|
||||
def _pick(exclude=None, prefer=None, n=6):
|
||||
ex = set(exclude or [])
|
||||
chosen = []
|
||||
# prefer에서 최대 3개 우선 선택
|
||||
for p in (prefer or []):
|
||||
if p not in ex and len(chosen) < 3:
|
||||
chosen.append(p)
|
||||
# 구간별 1개씩 (갭 우선)
|
||||
for lo, hi in [(1, 9), (10, 19), (20, 29), (30, 39), (40, 45)]:
|
||||
if len(chosen) >= n:
|
||||
break
|
||||
cands = [x for x in range(lo, hi + 1) if x not in ex and x not in chosen]
|
||||
if cands:
|
||||
chosen.append(max(cands, key=lambda x: gap_w.get(x, 0)))
|
||||
# 부족하면 나머지에서 갭 순
|
||||
rest = sorted(
|
||||
[x for x in range(1, 46) if x not in ex and x not in chosen],
|
||||
key=lambda x: -gap_w.get(x, 0),
|
||||
)
|
||||
while len(chosen) < n and rest:
|
||||
chosen.append(rest.pop(0))
|
||||
return sorted(chosen[:n])
|
||||
|
||||
set1 = _pick(exclude=hot_numbers[:5], prefer=overdue_numbers[:5])
|
||||
set2 = _pick()
|
||||
set3 = _pick(exclude=hot_numbers)
|
||||
|
||||
# 신뢰도 점수
|
||||
data_vol = min(total_draws / 500, 1.0)
|
||||
if len(recent_sums) > 1:
|
||||
avg_s = sum(recent_sums) / len(recent_sums)
|
||||
std_s = (sum((s - avg_s) ** 2 for s in recent_sums) / len(recent_sums)) ** 0.5
|
||||
pattern = max(0.0, 1.0 - std_s / 60.0)
|
||||
else:
|
||||
pattern = 0.5
|
||||
trend = max(0.0, 1.0 - len(hot_numbers) / max(len(r10_nums), 1))
|
||||
confidence = round((data_vol * 0.4 + pattern * 0.35 + trend * 0.25) * 100)
|
||||
|
||||
return {
|
||||
"target_drw_no": target_drw_no,
|
||||
"based_on_draw": draws[-1][0],
|
||||
"generated_at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
||||
"hot_numbers": hot_numbers[:8],
|
||||
"cold_numbers": cold_numbers,
|
||||
"overdue_numbers": overdue_numbers,
|
||||
"recent_pattern": {
|
||||
"last3_numbers": sorted(set(r3_nums)),
|
||||
"triple_appear": triple_appear,
|
||||
"recent_sum_avg": round(sum(recent_sums) / len(recent_sums), 1) if recent_sums else 0,
|
||||
"recent_odd_avg": round(sum(recent_odd) / len(recent_odd), 1) if recent_odd else 0,
|
||||
},
|
||||
"recommended_sets": [
|
||||
{"strategy": "냉각번호 중심", "numbers": set1, "description": "오랫동안 미출현 번호 위주 + 과출현 제외"},
|
||||
{"strategy": "균형형", "numbers": set2, "description": "구간 균형 + 갭 최적화"},
|
||||
{"strategy": "과출현 피하기", "numbers": set3, "description": "최근 자주 나온 번호 완전 제외"},
|
||||
],
|
||||
"confidence_score": confidence,
|
||||
"confidence_factors": {
|
||||
"data_volume": round(data_vol * 100),
|
||||
"pattern_consistency": round(pattern * 100),
|
||||
"recent_trend": round(trend * 100),
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user