lotto lab 추천 알고리즘 및 시뮬레이션 강화
This commit is contained in:
@@ -77,6 +77,72 @@ def init_db() -> None:
|
||||
# ✅ UNIQUE 인덱스(중복 저장 방지)
|
||||
conn.execute("CREATE UNIQUE INDEX IF NOT EXISTS uq_reco_dedup ON recommendations(dedup_hash);")
|
||||
|
||||
# ── 시뮬레이션 테이블 ─────────────────────────────────────────────────
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS simulation_runs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
run_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
strategy TEXT NOT NULL DEFAULT 'monte_carlo',
|
||||
total_generated INTEGER NOT NULL DEFAULT 0,
|
||||
top_k_selected INTEGER NOT NULL DEFAULT 0,
|
||||
avg_score REAL,
|
||||
notes TEXT DEFAULT ''
|
||||
);
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_simrun_at ON simulation_runs(run_at DESC);"
|
||||
)
|
||||
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS simulation_candidates (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
run_id INTEGER NOT NULL,
|
||||
numbers TEXT NOT NULL,
|
||||
score_total REAL NOT NULL,
|
||||
score_frequency REAL,
|
||||
score_fingerprint REAL,
|
||||
score_gap REAL,
|
||||
score_cooccur REAL,
|
||||
score_diversity REAL,
|
||||
is_best INTEGER DEFAULT 0,
|
||||
based_on_draw INTEGER,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
FOREIGN KEY(run_id) REFERENCES simulation_runs(id)
|
||||
);
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_simcand_run "
|
||||
"ON simulation_candidates(run_id, score_total DESC);"
|
||||
)
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_simcand_best "
|
||||
"ON simulation_candidates(is_best, score_total DESC);"
|
||||
)
|
||||
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS best_picks (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
numbers TEXT NOT NULL,
|
||||
score_total REAL NOT NULL,
|
||||
rank_in_run INTEGER,
|
||||
source_run_id INTEGER,
|
||||
based_on_draw INTEGER,
|
||||
is_active INTEGER DEFAULT 1,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
FOREIGN KEY(source_run_id) REFERENCES simulation_runs(id)
|
||||
);
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_bestpicks_active "
|
||||
"ON best_picks(is_active, score_total DESC);"
|
||||
)
|
||||
|
||||
def upsert_draw(row: Dict[str, Any]) -> None:
|
||||
with _conn() as conn:
|
||||
conn.execute(
|
||||
@@ -276,11 +342,160 @@ def update_recommendation_result(rec_id: int, rank: int, correct_count: int, has
|
||||
with _conn() as conn:
|
||||
cur = conn.execute(
|
||||
"""
|
||||
UPDATE recommendations
|
||||
SET rank = ?, correct_count = ?, has_bonus = ?, checked = 1
|
||||
UPDATE recommendations
|
||||
SET rank = ?, correct_count = ?, has_bonus = ?, checked = 1
|
||||
WHERE id = ?
|
||||
""",
|
||||
(rank, correct_count, 1 if has_bonus else 0, rec_id)
|
||||
)
|
||||
return cur.rowcount > 0
|
||||
|
||||
|
||||
# ── 시뮬레이션 CRUD ─────────────────────────────────────────────────────────
|
||||
|
||||
def save_simulation_run(
|
||||
strategy: str,
|
||||
total_generated: int,
|
||||
top_k_selected: int,
|
||||
avg_score: float,
|
||||
notes: str = "",
|
||||
) -> int:
|
||||
"""시뮬레이션 실행 기록 저장, 생성된 ID 반환"""
|
||||
with _conn() as conn:
|
||||
cur = conn.execute(
|
||||
"""
|
||||
INSERT INTO simulation_runs (strategy, total_generated, top_k_selected, avg_score, notes)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
""",
|
||||
(strategy, total_generated, top_k_selected, round(avg_score, 6), notes),
|
||||
)
|
||||
return int(cur.lastrowid)
|
||||
|
||||
|
||||
def save_simulation_candidates_bulk(
|
||||
run_id: int,
|
||||
candidates: List[Dict[str, Any]],
|
||||
based_on_draw: Optional[int],
|
||||
) -> None:
|
||||
"""
|
||||
상위 후보들을 simulation_candidates 테이블에 일괄 저장.
|
||||
candidates 각 항목: {"numbers": [...], "score_total": ..., "score_*": ..., "is_best": bool}
|
||||
"""
|
||||
data = [
|
||||
(
|
||||
run_id,
|
||||
json.dumps(sorted(c["numbers"])),
|
||||
c["score_total"],
|
||||
c.get("score_frequency"),
|
||||
c.get("score_fingerprint"),
|
||||
c.get("score_gap"),
|
||||
c.get("score_cooccur"),
|
||||
c.get("score_diversity"),
|
||||
1 if c.get("is_best") else 0,
|
||||
based_on_draw,
|
||||
)
|
||||
for c in candidates
|
||||
]
|
||||
with _conn() as conn:
|
||||
conn.executemany(
|
||||
"""
|
||||
INSERT INTO simulation_candidates
|
||||
(run_id, numbers, score_total, score_frequency, score_fingerprint,
|
||||
score_gap, score_cooccur, score_diversity, is_best, based_on_draw)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
data,
|
||||
)
|
||||
|
||||
|
||||
def replace_best_picks(
|
||||
picks: List[Dict[str, Any]],
|
||||
run_id: int,
|
||||
based_on_draw: Optional[int],
|
||||
) -> None:
|
||||
"""
|
||||
기존 활성 best_picks를 비활성화하고 새 picks로 교체.
|
||||
picks 각 항목: {"numbers": [...], "score_total": ..., "rank_in_run": int}
|
||||
"""
|
||||
with _conn() as conn:
|
||||
conn.execute("UPDATE best_picks SET is_active = 0 WHERE is_active = 1")
|
||||
data = [
|
||||
(
|
||||
json.dumps(sorted(p["numbers"])),
|
||||
p["score_total"],
|
||||
p.get("rank_in_run"),
|
||||
run_id,
|
||||
based_on_draw,
|
||||
)
|
||||
for p in picks
|
||||
]
|
||||
conn.executemany(
|
||||
"""
|
||||
INSERT INTO best_picks (numbers, score_total, rank_in_run, source_run_id, based_on_draw, is_active)
|
||||
VALUES (?, ?, ?, ?, ?, 1)
|
||||
""",
|
||||
data,
|
||||
)
|
||||
|
||||
|
||||
def get_best_picks(limit: int = 20) -> List[Dict[str, Any]]:
|
||||
"""현재 활성화된 best_picks 조회 (점수 내림차순)"""
|
||||
with _conn() as conn:
|
||||
rows = conn.execute(
|
||||
"""
|
||||
SELECT id, numbers, score_total, rank_in_run, source_run_id, based_on_draw, created_at
|
||||
FROM best_picks
|
||||
WHERE is_active = 1
|
||||
ORDER BY score_total DESC
|
||||
LIMIT ?
|
||||
""",
|
||||
(limit,),
|
||||
).fetchall()
|
||||
return [
|
||||
{
|
||||
"id": int(r["id"]),
|
||||
"numbers": json.loads(r["numbers"]),
|
||||
"score_total": r["score_total"],
|
||||
"rank_in_run": r["rank_in_run"],
|
||||
"source_run_id": r["source_run_id"],
|
||||
"based_on_draw": r["based_on_draw"],
|
||||
"created_at": r["created_at"],
|
||||
}
|
||||
for r in rows
|
||||
]
|
||||
|
||||
|
||||
def get_simulation_runs(limit: int = 10) -> List[Dict[str, Any]]:
|
||||
"""최근 시뮬레이션 실행 기록 조회"""
|
||||
with _conn() as conn:
|
||||
rows = conn.execute(
|
||||
"""
|
||||
SELECT id, run_at, strategy, total_generated, top_k_selected, avg_score, notes
|
||||
FROM simulation_runs
|
||||
ORDER BY id DESC
|
||||
LIMIT ?
|
||||
""",
|
||||
(limit,),
|
||||
).fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
|
||||
def get_simulation_candidates(run_id: int, limit: int = 100) -> List[Dict[str, Any]]:
|
||||
"""특정 시뮬레이션 실행의 후보 목록 조회 (점수 내림차순)"""
|
||||
with _conn() as conn:
|
||||
rows = conn.execute(
|
||||
"""
|
||||
SELECT id, numbers, score_total, score_frequency, score_fingerprint,
|
||||
score_gap, score_cooccur, score_diversity, is_best, based_on_draw, created_at
|
||||
FROM simulation_candidates
|
||||
WHERE run_id = ?
|
||||
ORDER BY score_total DESC
|
||||
LIMIT ?
|
||||
""",
|
||||
(run_id, limit),
|
||||
).fetchall()
|
||||
return [
|
||||
{**dict(r), "numbers": json.loads(r["numbers"])}
|
||||
for r in rows
|
||||
]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user