05/23 update prediction accuration

This commit is contained in:
2026-05-23 17:50:15 -04:00
parent 59789f3cfe
commit c61c09db94
5 changed files with 408 additions and 17 deletions
+110 -16
View File
@@ -20,6 +20,9 @@ import numpy as np
from db.models import get_all_draws_numbers, get_game_by_id
from core.analyzer import frequency_analysis, gap_analysis, positional_frequency
from core.filters import passes_filters, sum_range_percentiles
_MAX_FILTER_TRIES = 50
# ── Internal helpers ──────────────────────────────────────────────────────────
@@ -53,6 +56,46 @@ def _hot_bonus(draws, game: dict) -> int | None:
return random.randint(1, game["bonus_max"])
def _retry_filter(generate_fn, sum_range, max_tries: int = _MAX_FILTER_TRIES) -> list[int]:
"""
Call generate_fn() up to max_tries times; return the first numbers list
that passes combination filters, or the last generated if none do.
Used for stochastic strategies where each call produces a new candidate.
"""
last = generate_fn()
if passes_filters(last, sum_range):
return last
for _ in range(max_tries - 1):
attempt = generate_fn()
if passes_filters(attempt, sum_range):
return attempt
return last
def _swap_filter(numbers: list[int], ranked_pool: list[int],
sum_range) -> list[int]:
"""
For deterministic strategies: try swapping the weakest-ranked number in
the ticket for the best available alternative until filters pass.
ranked_pool must contain candidates sorted best-first, excluding numbers
already in the ticket.
Returns the first passing combination, or the original if none found.
"""
if passes_filters(numbers, sum_range):
return numbers
ticket = list(numbers)
for swap_idx in range(len(ticket) - 1, -1, -1):
original = ticket[swap_idx]
for alt in ranked_pool:
if alt not in ticket:
ticket[swap_idx] = alt
candidate = sorted(ticket)
if passes_filters(candidate, sum_range):
return candidate
ticket[swap_idx] = original
return numbers # graceful fallback: return original if no swap helped
def _fill_to_count(chosen: list, game: dict, exclude: set | None = None) -> list:
"""Pad chosen with random unused numbers if fewer than main_count."""
excl = exclude or set()
@@ -73,6 +116,7 @@ def hot_numbers(game_id: int, last_n: int = 100, exclude: set | None = None) ->
Top main_count most-frequent numbers from the last last_n draws.
Bonus: most frequent historical bonus ball.
Falls back to random if there is no history.
Applies combination filters; swaps the weakest pick if the ticket is weak.
"""
excl = exclude or set()
game = get_game_by_id(game_id)
@@ -86,10 +130,15 @@ def hot_numbers(game_id: int, last_n: int = 100, exclude: set | None = None) ->
for draw in recent:
counter.update(draw["numbers"])
top = [n for n, _ in sorted(counter.items(), key=lambda kv: (-kv[1], kv[0]))
if n not in excl]
numbers = _fill_to_count(top[: game["main_count"]], game, excl)
bonus = _hot_bonus(draws, game)
ranked = [n for n, _ in sorted(counter.items(), key=lambda kv: (-kv[1], kv[0]))
if n not in excl]
numbers = _fill_to_count(ranked[: game["main_count"]], game, excl)
sum_range = sum_range_percentiles(game_id)
ranked_pool = [n for n in ranked if n not in numbers]
numbers = _swap_filter(numbers, ranked_pool, sum_range)
bonus = _hot_bonus(draws, game)
return {"numbers": numbers, "bonus": bonus}
@@ -99,6 +148,7 @@ def due_numbers(game_id: int, exclude: set | None = None) -> dict:
"""
Numbers with the largest gap (most overdue) based on historical frequency.
Falls back to random if there is no history.
Applies combination filters; swaps the lowest-gap pick if the ticket is weak.
"""
excl = exclude or set()
game = get_game_by_id(game_id)
@@ -106,10 +156,15 @@ def due_numbers(game_id: int, exclude: set | None = None) -> dict:
if not gaps:
return _random_ticket(game, excl)
top = [n for n, _ in sorted(gaps.items(), key=lambda kv: (-kv[1], kv[0]))
if n not in excl]
numbers = _fill_to_count(top[: game["main_count"]], game, excl)
bonus = _random_bonus(game)
ranked = [n for n, _ in sorted(gaps.items(), key=lambda kv: (-kv[1], kv[0]))
if n not in excl]
numbers = _fill_to_count(ranked[: game["main_count"]], game, excl)
sum_range = sum_range_percentiles(game_id)
ranked_pool = [n for n in ranked if n not in numbers]
numbers = _swap_filter(numbers, ranked_pool, sum_range)
bonus = _random_bonus(game)
return {"numbers": numbers, "bonus": bonus}
@@ -120,6 +175,7 @@ def weighted_random(game_id: int, exclude: set | None = None) -> dict:
Random draw with probability proportional to historical frequency.
Numbers that have never appeared receive a minimum weight of 1
so they remain in contention.
Retries up to _MAX_FILTER_TRIES times to find a combination-quality ticket.
"""
excl = exclude or set()
game = get_game_by_id(game_id)
@@ -129,9 +185,15 @@ def weighted_random(game_id: int, exclude: set | None = None) -> dict:
weights = np.array([freq.get(n, 1) for n in pool], dtype=float)
weights /= weights.sum()
chosen = np.random.choice(pool, size=game["main_count"], replace=False, p=weights)
bonus = _random_bonus(game)
return {"numbers": sorted(chosen.tolist()), "bonus": bonus}
sum_range = sum_range_percentiles(game_id)
def _generate():
chosen = np.random.choice(pool, size=game["main_count"], replace=False, p=weights)
return sorted(chosen.tolist())
numbers = _retry_filter(_generate, sum_range)
bonus = _random_bonus(game)
return {"numbers": numbers, "bonus": bonus}
# ── Strategy 4: Monte Carlo ───────────────────────────────────────────────────
@@ -141,6 +203,7 @@ def monte_carlo(game_id: int, simulations: int = 10_000,
"""
Run `simulations` weighted-random draws; tally how often each number
is selected; return the top main_count by tally count.
Applies combination filters; swaps the lowest-tally pick if ticket is weak.
"""
excl = exclude or set()
game = get_game_by_id(game_id)
@@ -155,9 +218,14 @@ def monte_carlo(game_id: int, simulations: int = 10_000,
ticket = np.random.choice(pool, size=game["main_count"], replace=False, p=weights)
tally.update(ticket.tolist())
top = [n for n, _ in tally.most_common(game["main_count"])]
numbers = _fill_to_count(top, game, excl)
bonus = _random_bonus(game)
ranked = [n for n, _ in tally.most_common()]
numbers = _fill_to_count(ranked[: game["main_count"]], game, excl)
sum_range = sum_range_percentiles(game_id)
ranked_pool = [n for n in ranked if n not in numbers]
numbers = _swap_filter(numbers, ranked_pool, sum_range)
bonus = _random_bonus(game)
return {"numbers": numbers, "bonus": bonus}
@@ -167,6 +235,7 @@ def positional_pick(game_id: int, exclude: set | None = None) -> dict:
"""
For each draw position, select the most frequently appearing number
that has not already been chosen for a previous position.
Applies combination filters; swaps the lowest-positional-rank pick if weak.
"""
excl = exclude or set()
game = get_game_by_id(game_id)
@@ -177,6 +246,8 @@ def positional_pick(game_id: int, exclude: set | None = None) -> dict:
selected = []
used = set()
# Also collect per-position alternates (next-best) for the swap pool
alternates_pool = []
for pos in range(1, game["main_count"] + 1):
freqs = pos_freq.get(pos, {})
@@ -189,12 +260,25 @@ def positional_pick(game_id: int, exclude: set | None = None) -> dict:
if not available:
available = [n for n in range(1, game["main_max"] + 1) if n not in used]
picked = random.choice(available)
else:
# Collect alternates for this position (for potential swap)
for n, _ in ranked:
if n != picked and n not in excl:
alternates_pool.append(n)
selected.append(picked)
used.add(picked)
numbers = sorted(selected)
sum_range = sum_range_percentiles(game_id)
# Alternates sorted by first-occurrence (positional best-first)
seen = set()
ranked_pool = [n for n in alternates_pool
if n not in numbers and not (seen.add(n) or n in seen)]
numbers = _swap_filter(numbers, ranked_pool, sum_range)
bonus = _random_bonus(game)
return {"numbers": sorted(selected), "bonus": bonus}
return {"numbers": numbers, "bonus": bonus}
# ── Strategy 6: Quick Pick ────────────────────────────────────────────────────
@@ -203,6 +287,16 @@ def quick_pick(game_id: int, exclude: set | None = None) -> dict:
"""
Pure random selection from the full number pool.
Requires no historical draw data.
Retries up to _MAX_FILTER_TRIES times to find a combination-quality ticket.
"""
excl = exclude or set()
game = get_game_by_id(game_id)
return _random_ticket(game, exclude or set())
sum_range = sum_range_percentiles(game_id)
def _generate():
pool = _safe_pool(game, excl)
return sorted(random.sample(pool, game["main_count"]))
numbers = _retry_filter(_generate, sum_range)
bonus = _random_bonus(game)
return {"numbers": numbers, "bonus": bonus}