로또 종합 추론 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]:
|
def generate_weekly_report(draws: List[Tuple[int, List[int]]], target_drw_no: int) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
특정 회차 공략 리포트 생성.
|
특정 회차 공략 리포트 생성.
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ from .collector import sync_latest, sync_ensure_all
|
|||||||
from .generator import run_simulation, generate_smart_recommendations
|
from .generator import run_simulation, generate_smart_recommendations
|
||||||
from .checker import check_results_for_draw
|
from .checker import check_results_for_draw
|
||||||
from .utils import calc_metrics, calc_recent_overlap
|
from .utils import calc_metrics, calc_recent_overlap
|
||||||
from .analyzer import get_statistical_report, generate_weekly_report, analyze_personal_patterns
|
from .analyzer import get_statistical_report, generate_weekly_report, analyze_personal_patterns, generate_combined_recommendation
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
scheduler = BackgroundScheduler(timezone=os.getenv("TZ", "Asia/Seoul"))
|
scheduler = BackgroundScheduler(timezone=os.getenv("TZ", "Asia/Seoul"))
|
||||||
@@ -389,6 +389,46 @@ def api_simulation(run_id: Optional[int] = None, runs_limit: int = 5):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ── 종합 추론 추천 ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@app.get("/api/lotto/recommend/combined")
|
||||||
|
def api_recommend_combined():
|
||||||
|
"""5가지 통계 기법 종합 추론 추천 — 결과를 이력에 저장한다."""
|
||||||
|
draws = get_all_draw_numbers()
|
||||||
|
if not draws:
|
||||||
|
raise HTTPException(status_code=404, detail="No data")
|
||||||
|
|
||||||
|
latest = get_latest_draw()
|
||||||
|
result = generate_combined_recommendation(draws)
|
||||||
|
if "error" in result:
|
||||||
|
raise HTTPException(status_code=500, detail=result["error"])
|
||||||
|
|
||||||
|
# 추천 이력 저장 (태그: 종합추론)
|
||||||
|
params = {"method": "combined"}
|
||||||
|
saved = save_recommendation_dedup(
|
||||||
|
latest["drw_no"] if latest else None,
|
||||||
|
result["final_numbers"],
|
||||||
|
params,
|
||||||
|
)
|
||||||
|
if saved["saved"]:
|
||||||
|
update_recommendation(saved["id"], tags=["종합추론"])
|
||||||
|
|
||||||
|
return {
|
||||||
|
**result,
|
||||||
|
"id": saved["id"],
|
||||||
|
"saved": saved["saved"],
|
||||||
|
"deduped": saved["deduped"],
|
||||||
|
"based_on_latest_draw": latest["drw_no"] if latest else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/lotto/recommend/combined/history")
|
||||||
|
def api_combined_history(limit: int = 30):
|
||||||
|
"""종합추론 추천 이력 조회."""
|
||||||
|
items = list_recommendations_ex(limit=limit, tag="종합추론", sort="id_desc")
|
||||||
|
return {"items": items, "total": len(items)}
|
||||||
|
|
||||||
|
|
||||||
# ── 기존 수동 추천 API (하위 호환 유지) ─────────────────────────────────────
|
# ── 기존 수동 추천 API (하위 호환 유지) ─────────────────────────────────────
|
||||||
@app.get("/api/lotto/recommend")
|
@app.get("/api/lotto/recommend")
|
||||||
def api_recommend(
|
def api_recommend(
|
||||||
|
|||||||
Reference in New Issue
Block a user