로또 종합 추론 API 추가 (5가지 통계 기법 가중 투표)

- analyzer.py: generate_combined_recommendation() 함수 추가
  빈도Z(25%)·조합지문(30%)·갭(20%)·공동출현(15%)·다양성(10%) 가중 투표
- main.py: GET /api/lotto/recommend/combined 엔드포인트 추가
  결과를 태그 "종합추론"으로 recommendations 테이블에 저장
- main.py: GET /api/lotto/recommend/combined/history 엔드포인트 추가

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-25 08:40:53 +09:00
parent 09e5ab4e30
commit c9737b380f
2 changed files with 167 additions and 1 deletions

View File

@@ -425,6 +425,132 @@ def analyze_personal_patterns(
}
def generate_combined_recommendation(draws: List[Tuple[int, List[int]]]) -> Dict[str, Any]:
"""
5가지 통계 기법을 종합한 추론 번호 추천.
각 기법이 상위 6개 번호를 추천하고, 기법별 가중치(score_combination 가중치와 동일)로
투표를 집계한 뒤 최종 6개 번호를 선정한다.
가중치: 빈도Z(25%) · 지문(30%) · 갭(20%) · 공동출현(15%) · 다양성(10%)
"""
if not draws:
return {"error": "데이터 없음"}
cache = build_analysis_cache(draws)
z = cache["z_scores"]
gap = cache["last_seen_gap"]
freq = cache["freq_all"]
cooccur = cache["cooccur"]
zone_probs = cache["zone_probs"]
# ── Method 1: 빈도 Z-score ────────────────────────────────────────────────
# Z-score 내림차순 상위 6 (출현 빈도가 기댓값보다 높은 번호)
m_frequency = sorted(range(1, 46), key=lambda n: -z.get(n, 0))[:6]
# ── Method 2: 갭 분석 ─────────────────────────────────────────────────────
# 가장 오래 미출현한 번호 6개 (오버듀)
m_gap = sorted(range(1, 46), key=lambda n: -gap.get(n, 0))[:6]
# ── Method 3: 공동출현 ────────────────────────────────────────────────────
# 각 번호의 총 공동출현 합산 점수 내림차순 6개
cooccur_total: Dict[int, float] = defaultdict(float)
for (a, b), cnt in cooccur.items():
cooccur_total[a] += cnt
cooccur_total[b] += cnt
m_cooccur = sorted(range(1, 46), key=lambda n: -cooccur_total.get(n, 0))[:6]
# ── Method 4: 조합 지문 ───────────────────────────────────────────────────
# 역대 당첨 조합의 구간별 최빈 분포에 맞게 각 구간에서 빈도 상위 번호 선택
zone_targets: List[int] = []
for zp in zone_probs:
zone_targets.append(max(zp, key=zp.get) if zp else 1)
# 합이 정확히 6이 되도록 조정
diff = sum(zone_targets) - 6
if diff > 0:
idxs = sorted(range(5), key=lambda i: -zone_targets[i])
for i in idxs:
if diff <= 0:
break
zone_targets[i] = max(0, zone_targets[i] - 1)
diff -= 1
elif diff < 0:
idxs = sorted(range(5), key=lambda i: zone_targets[i])
for i in idxs:
if diff >= 0:
break
zone_targets[i] += 1
diff += 1
m_fingerprint: List[int] = []
for (lo, hi), tgt in zip(ZONE_RANGES, zone_targets):
zone_nums = sorted(range(lo, hi + 1), key=lambda x: -freq.get(x, 0))
m_fingerprint.extend(zone_nums[:tgt])
m_fingerprint = sorted(m_fingerprint[:6])
# ── Method 5: 다양성 ──────────────────────────────────────────────────────
# 각 구간에서 갭 가장 큰 번호 1개씩 (5개) + 전체 갭 상위 1개 보충
m_diversity: List[int] = []
for lo, hi in ZONE_RANGES:
zone_nums = sorted(range(lo, hi + 1), key=lambda n: -gap.get(n, 0))
if zone_nums:
m_diversity.append(zone_nums[0])
if len(m_diversity) < 6:
rest = sorted(
[x for x in range(1, 46) if x not in m_diversity],
key=lambda n: -gap.get(n, 0),
)
m_diversity.extend(rest[: 6 - len(m_diversity)])
m_diversity = sorted(m_diversity[:6])
# ── 가중 투표 집계 ────────────────────────────────────────────────────────
# score_combination 가중치와 동일: 빈도25, 지문30, 갭20, 공동출현15, 다양성10
method_entries = [
(m_frequency, 25, "frequency", "빈도 Z-score"),
(m_fingerprint, 30, "fingerprint", "조합 지문"),
(m_gap, 20, "gap", "갭 분석"),
(m_cooccur, 15, "cooccur", "공동 출현"),
(m_diversity, 10, "diversity", "다양성"),
]
vote_scores: Dict[int, float] = {n: 0.0 for n in range(1, 46)}
for method_nums, weight, _, _ in method_entries:
for rank, n in enumerate(method_nums):
# rank 0 = 1위: (6-0)×weight = 6w, rank 5 = 6위: (6-5)×weight = w
vote_scores[n] += (6 - rank) * weight
# 상위 6개 — 동점 시 Z-score 타이브레이크
final_numbers = sorted(
sorted(range(1, 46), key=lambda n: (-vote_scores[n], -z.get(n, 0)))[:6]
)
scores = score_combination(final_numbers, cache)
# 각 번호가 몇 개 방법에서 채택됐는지
vote_counts: Dict[str, int] = {
str(n): sum(1 for nums, _, _, _ in method_entries if n in nums)
for n in range(1, 46)
}
methods_result: Dict[str, Any] = {}
for nums, weight, key, label in method_entries:
methods_result[key] = {
"label": label,
"weight_pct": weight,
"numbers": sorted(nums),
}
return {
"methods": methods_result,
"final_numbers": final_numbers,
"scores": scores,
"vote_scores": {str(n): round(vote_scores[n], 1) for n in range(1, 46)},
"vote_counts": vote_counts,
"total_draws": cache["total_draws"],
}
def generate_weekly_report(draws: List[Tuple[int, List[int]]], target_drw_no: int) -> Dict[str, Any]:
"""
특정 회차 공략 리포트 생성.