로또 종합 추론 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:
@@ -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]:
|
||||
"""
|
||||
특정 회차 공략 리포트 생성.
|
||||
|
||||
Reference in New Issue
Block a user