Jean Marco Cruz Castellar commited on
Commit
5cbc4a2
·
unverified ·
1 Parent(s): 3c8e698

Add files via upload

Browse files
proyecto_tinka_ml/proyecto_tinka_ml/README.md ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Sistema de Análisis y Generación de Combinaciones — Tinka (con ML)
2
+
3
+ **Aviso:** La lotería es aleatoria. Este proyecto es **educativo**; no garantiza aciertos.
4
+
5
+ ## 1) Requisitos
6
+ - Python 3.9+
7
+ - Instala dependencias:
8
+ ```bash
9
+ pip install -r requirements.txt
10
+ ```
11
+
12
+ ## 2) Configurar BD
13
+ Edita `config.py` con tus credenciales (XAMPP por defecto). La tabla `resultados` debe tener:
14
+ `id_sorteo`, `fecha_sorteo`, `numeros`, `boliyapa`, `jackpot`, `created_at` (opcional).
15
+
16
+ ## 3) Ejecutar
17
+ ```bash
18
+ python main.py
19
+ ```
20
+
21
+ ## 4) Menú
22
+ 1. Dashboard de estadísticas (exporta `data/reporte.html`)
23
+ 2. Análisis por número
24
+ 3. Generar combinaciones (elige estrategia y **cantidad**)
25
+ 4. Evaluar tu combinación
26
+ 5. Mejores históricas por score heurístico
27
+ 6. Exportar análisis a HTML
28
+ 7. Refrescar datos desde la BD (actualiza caché)
29
+ 8. **Recomendación automática (ML)**: calcula probabilidades bayesianas (globales + recientes), genera un pool (estrategias clásicas + **Thompson Sampling**) y rankea con un score ML (log-prob + heurísticas). Guarda:
30
+ - `data/probabilidades.json`
31
+ - `data/combinaciones_generadas.json`
32
+
33
+ ## 5) Notas
34
+ - Cambios robustos en caché (JSON atómico).
35
+ - SQL con SQLAlchemy para evitar warnings.
36
+ - Visualización simple con matplotlib (PNG embebido).
proyecto_tinka_ml/proyecto_tinka_ml/__pycache__/analizador.cpython-313.pyc ADDED
Binary file (11.8 kB). View file
 
proyecto_tinka_ml/proyecto_tinka_ml/__pycache__/config.cpython-313.pyc ADDED
Binary file (877 Bytes). View file
 
proyecto_tinka_ml/proyecto_tinka_ml/__pycache__/db_connector.cpython-313.pyc ADDED
Binary file (3.98 kB). View file
 
proyecto_tinka_ml/proyecto_tinka_ml/__pycache__/generador.cpython-313.pyc ADDED
Binary file (12.7 kB). View file
 
proyecto_tinka_ml/proyecto_tinka_ml/__pycache__/ml.cpython-313.pyc ADDED
Binary file (6.19 kB). View file
 
proyecto_tinka_ml/proyecto_tinka_ml/__pycache__/utils.cpython-313.pyc ADDED
Binary file (3.08 kB). View file
 
proyecto_tinka_ml/proyecto_tinka_ml/__pycache__/visualizador.cpython-313.pyc ADDED
Binary file (3.64 kB). View file
 
proyecto_tinka_ml/proyecto_tinka_ml/analizador.py ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Lógica de análisis estadístico para Tinka/Boliyapa."""
2
+ from __future__ import annotations
3
+ from typing import Dict, List, Tuple
4
+ import numpy as np
5
+ import pandas as pd
6
+ from itertools import combinations
7
+ from collections import Counter
8
+ from config import TOTAL_NUMBERS, COMBINATION_SIZE, LAST_N_WINDOWS, SUM_RANGE
9
+ from utils import parse_numbers, is_prime
10
+
11
+ def _explode_numeros(df: pd.DataFrame) -> pd.DataFrame:
12
+ rows = []
13
+ for _, r in df.iterrows():
14
+ nums = parse_numbers(r['numeros'])
15
+ for n in nums:
16
+ rows.append({'id_sorteo': r['id_sorteo'], 'fecha_sorteo': r['fecha_sorteo'], 'numero': n})
17
+ return pd.DataFrame(rows, columns=['id_sorteo','fecha_sorteo','numero'])
18
+
19
+ def _coocurrencias(df: pd.DataFrame, k: int = 2):
20
+ cnt = Counter()
21
+ for _, r in df.iterrows():
22
+ arr = sorted(parse_numbers(r['numeros']))
23
+ for combo in combinations(arr, k):
24
+ cnt[combo] += 1
25
+ return cnt
26
+
27
+ def _ultimos(df: pd.DataFrame, n: int):
28
+ return df.tail(n) if len(df) >= n else df.copy()
29
+
30
+ def analisis_frecuencias(df: pd.DataFrame) -> Dict:
31
+ exploded = _explode_numeros(df)
32
+ freq_abs = Counter(exploded['numero'].tolist())
33
+ for k in range(1, TOTAL_NUMBERS+1):
34
+ freq_abs.setdefault(k, 0)
35
+ total_bolas = len(exploded)
36
+ freq_rel = {k: (v/total_bolas if total_bolas else 0.0) for k, v in freq_abs.items()}
37
+ top = sorted(freq_abs.items(), key=lambda x: (-x[1], x[0]))
38
+ hot = [k for k,_ in top[:15]]
39
+ cold = [k for k,_ in sorted(freq_abs.items(), key=lambda x: (x[1], x[0]))[:15]]
40
+ ult10 = set(_explode_numeros(_ultimos(df, 10))['numero'].tolist())
41
+ # dormidos: no aparecen hace >20 sorteos
42
+ last_seen_idx = {}
43
+ for idx, r in enumerate(df.itertuples(index=False), start=1):
44
+ for n in parse_numbers(r.numeros):
45
+ last_seen_idx[n] = idx
46
+ dormidos = []
47
+ threshold = max(1, len(df) - 20)
48
+ for n in range(1, TOTAL_NUMBERS+1):
49
+ if last_seen_idx.get(n, 0) < threshold:
50
+ dormidos.append(n)
51
+ return {'freq_abs': dict(sorted(freq_abs.items())),
52
+ 'freq_rel': dict(sorted(freq_rel.items())),
53
+ 'hot_15': hot,
54
+ 'cold_15': cold,
55
+ 'en_racha_ult10': sorted(ult10),
56
+ 'dormidos_mas20': sorted(dormidos)}
57
+
58
+ def analisis_temporal(df: pd.DataFrame) -> Dict:
59
+ exploded = _explode_numeros(df)
60
+ tmp = exploded.copy()
61
+ tmp['fecha_sorteo'] = pd.to_datetime(tmp['fecha_sorteo'])
62
+ tmp['anio'] = tmp['fecha_sorteo'].dt.year
63
+ tmp['mes'] = tmp['fecha_sorteo'].dt.month
64
+ by_mes = tmp.groupby(['anio','mes','numero']).size().reset_index(name='freq')
65
+ gaps = {}
66
+ for n in range(1, TOTAL_NUMBERS+1):
67
+ apar = tmp.loc[tmp['numero']==n, 'fecha_sorteo'].sort_values().tolist()
68
+ if len(apar) >= 2:
69
+ difs = [(apar[i]-apar[i-1]).days for i in range(1, len(apar))]
70
+ gaps[n] = float(np.mean(difs))
71
+ else:
72
+ gaps[n] = None
73
+ ventanas = {}
74
+ for win in LAST_N_WINDOWS:
75
+ sub = _ultimos(df, win)
76
+ ventanas[str(win)] = analisis_frecuencias(sub)
77
+ return {'por_mes': by_mes.to_dict(orient='records'), 'gaps_prom_dias': gaps, 'ventanas': ventanas}
78
+
79
+ def analisis_patrones(df: pd.DataFrame) -> Dict:
80
+ even, odd = 0, 0
81
+ ranges = {'1-9':0,'10-18':0,'19-27':0,'28-36':0,'37-45':0}
82
+ suma_vals = []
83
+ consecutivos = 0
84
+ distancias = []
85
+ primos_cnt = 0
86
+ total_combinaciones = 0
87
+ for _, r in df.iterrows():
88
+ arr = sorted(parse_numbers(r['numeros']))
89
+ total_combinaciones += 1
90
+ suma_vals.append(sum(arr))
91
+ for x in arr:
92
+ if x % 2 == 0: even += 1
93
+ else: odd += 1
94
+ if 1 <= x <= 9: ranges['1-9'] += 1
95
+ elif 10 <= x <= 18: ranges['10-18'] += 1
96
+ elif 19 <= x <= 27: ranges['19-27'] += 1
97
+ elif 28 <= x <= 36: ranges['28-36'] += 1
98
+ elif 37 <= x <= 45: ranges['37-45'] += 1
99
+ if is_prime(x): primos_cnt += 1
100
+ consecutivos += sum(1 for a,b in zip(arr, arr[1:]) if b==a+1)
101
+ distancias += [abs(b-a) for a,b in zip(arr, arr[1:])]
102
+ return {'pares': even, 'impares': odd, 'rangos': ranges,
103
+ 'suma_min': int(min(suma_vals)) if suma_vals else None,
104
+ 'suma_max': int(max(suma_vals)) if suma_vals else None,
105
+ 'suma_prom': float(np.mean(suma_vals)) if suma_vals else None,
106
+ 'consecutivos_total': int(consecutivos),
107
+ 'dist_prom': float(np.mean(distancias)) if distancias else None,
108
+ 'primos_total': int(primos_cnt),
109
+ 'total_combinaciones': total_combinaciones}
110
+
111
+ def analisis_coocurrencias(df: pd.DataFrame) -> Dict:
112
+ pairs = _coocurrencias(df, k=2)
113
+ trios = _coocurrencias(df, k=3)
114
+ top_pairs = pairs.most_common(20)
115
+ top_trios = trios.most_common(20)
116
+ return {'pairs_top20': [{'pair': list(k), 'freq': v} for k,v in top_pairs],
117
+ 'trios_top20': [{'trio': list(k), 'freq': v} for k,v in top_trios]}
118
+
119
+ def analisis_boliyapa(df: pd.DataFrame) -> Dict:
120
+ bol = df['boliyapa'].dropna().astype(int).tolist()
121
+ cnt = Counter(bol)
122
+ total = len(bol)
123
+ freq_rel = {k: (v/total if total else 0.0) for k,v in cnt.items()}
124
+ return {'freq_abs': dict(sorted(cnt.items())),
125
+ 'freq_rel': dict(sorted(freq_rel.items()))}
126
+
127
+ def analisis_chicuadrado(df: pd.DataFrame) -> Dict:
128
+ exploded = []
129
+ for _, r in df.iterrows():
130
+ exploded += parse_numbers(r['numeros'])
131
+ from collections import Counter
132
+ cnt = Counter(exploded)
133
+ import numpy as np
134
+ obs = np.array([cnt.get(i, 0) for i in range(1, TOTAL_NUMBERS+1)], dtype=float)
135
+ n = obs.sum()
136
+ if n == 0:
137
+ return {'chi2': None, 'p_value': None, 'expected': None}
138
+ expected = np.ones_like(obs) * (n / TOTAL_NUMBERS)
139
+ chi2 = ((obs - expected)**2 / expected).sum()
140
+ p_value = None
141
+ try:
142
+ from scipy.stats import chi2 as chi2_dist
143
+ dfree = TOTAL_NUMBERS - 1
144
+ p_value = float(1 - chi2_dist.cdf(chi2, dfree))
145
+ except Exception:
146
+ p_value = None
147
+ std_dev = float(np.std(obs))
148
+ return {'chi2': float(chi2), 'p_value': p_value, 'std_freq': std_dev, 'expected_each': float(n / TOTAL_NUMBERS)}
149
+
150
+ def analisis_completo(df: pd.DataFrame) -> Dict:
151
+ return {'frecuencias': analisis_frecuencias(df),
152
+ 'temporal': analisis_temporal(df),
153
+ 'patrones': analisis_patrones(df),
154
+ 'coocurrencias': analisis_coocurrencias(df),
155
+ 'boliyapa': analisis_boliyapa(df),
156
+ 'chi_cuadrado': analisis_chicuadrado(df)}
proyecto_tinka_ml/proyecto_tinka_ml/config.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Configuración del sistema Tinka/Boliyapa."""
2
+ from pathlib import Path
3
+
4
+ # === BD (XAMPP por defecto) ===
5
+ DB_CONFIG = {
6
+ "host": "localhost",
7
+ "port": 3306,
8
+ "database": "tinka_db",
9
+ "user": "root",
10
+ "password": ""
11
+ }
12
+
13
+ # Tabla con los resultados
14
+ TABLE_NAME = "resultados"
15
+
16
+ # === Carpetas ===
17
+ BASE_DIR = Path(__file__).resolve().parent
18
+ DATA_DIR = BASE_DIR / "data"
19
+ LOGS_DIR = BASE_DIR / "logs"
20
+
21
+ # Archivos de datos
22
+ CACHE_FILE = DATA_DIR / "cache_sorteos.json"
23
+ COMBOS_FILE = DATA_DIR / "combinaciones_generadas.json"
24
+
25
+ # Parámetros generales
26
+ TOTAL_NUMBERS = 45
27
+ COMBINATION_SIZE = 6
28
+
29
+ # Parámetros heurísticos
30
+ SUM_RANGE = (90, 180)
31
+ LAST_N_WINDOWS = [50, 100, 200]
proyecto_tinka_ml/proyecto_tinka_ml/data/cache_sorteos.json ADDED
@@ -0,0 +1,242 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "id_sorteo": 1206,
4
+ "fecha_sorteo": "2025-06-25",
5
+ "numeros": "38 30 08 27 48 14",
6
+ "boliyapa": 17,
7
+ "jackpot": 0.0,
8
+ "created_at": "2025-10-08 14:38:07"
9
+ },
10
+ {
11
+ "id_sorteo": 1207,
12
+ "fecha_sorteo": "2025-06-29",
13
+ "numeros": "47 37 32 22 24 23",
14
+ "boliyapa": 4,
15
+ "jackpot": 0.0,
16
+ "created_at": "2025-10-08 14:38:08"
17
+ },
18
+ {
19
+ "id_sorteo": 1208,
20
+ "fecha_sorteo": "2025-07-02",
21
+ "numeros": "14 28 32 07 10 29",
22
+ "boliyapa": 36,
23
+ "jackpot": 0.0,
24
+ "created_at": "2025-10-08 14:55:10"
25
+ },
26
+ {
27
+ "id_sorteo": 1209,
28
+ "fecha_sorteo": "2025-07-06",
29
+ "numeros": "29 20 14 11 03 15",
30
+ "boliyapa": 35,
31
+ "jackpot": 0.0,
32
+ "created_at": "2025-10-08 14:55:10"
33
+ },
34
+ {
35
+ "id_sorteo": 1210,
36
+ "fecha_sorteo": "2025-07-09",
37
+ "numeros": "35 28 09 42 31 16",
38
+ "boliyapa": 45,
39
+ "jackpot": 0.0,
40
+ "created_at": "2025-10-08 14:55:11"
41
+ },
42
+ {
43
+ "id_sorteo": 1211,
44
+ "fecha_sorteo": "2025-07-13",
45
+ "numeros": "09 32 43 49 38 24",
46
+ "boliyapa": 5,
47
+ "jackpot": 0.0,
48
+ "created_at": "2025-10-08 14:38:10"
49
+ },
50
+ {
51
+ "id_sorteo": 1212,
52
+ "fecha_sorteo": "2025-07-16",
53
+ "numeros": "36 22 37 11 07 01",
54
+ "boliyapa": 24,
55
+ "jackpot": 0.0,
56
+ "created_at": "2025-10-08 14:38:11"
57
+ },
58
+ {
59
+ "id_sorteo": 1213,
60
+ "fecha_sorteo": "2025-07-20",
61
+ "numeros": "42 45 28 06 19 05",
62
+ "boliyapa": 10,
63
+ "jackpot": 0.0,
64
+ "created_at": "2025-10-08 14:38:12"
65
+ },
66
+ {
67
+ "id_sorteo": 1214,
68
+ "fecha_sorteo": "2025-07-23",
69
+ "numeros": "22 13 24 43 02 01",
70
+ "boliyapa": 21,
71
+ "jackpot": 0.0,
72
+ "created_at": "2025-10-08 14:38:13"
73
+ },
74
+ {
75
+ "id_sorteo": 1215,
76
+ "fecha_sorteo": "2025-07-27",
77
+ "numeros": "36 24 04 21 34 09",
78
+ "boliyapa": 17,
79
+ "jackpot": 0.0,
80
+ "created_at": "2025-10-08 14:38:15"
81
+ },
82
+ {
83
+ "id_sorteo": 1216,
84
+ "fecha_sorteo": "2025-07-30",
85
+ "numeros": "43 28 03 09 01 25",
86
+ "boliyapa": 23,
87
+ "jackpot": 0.0,
88
+ "created_at": "2025-10-08 14:38:16"
89
+ },
90
+ {
91
+ "id_sorteo": 1217,
92
+ "fecha_sorteo": "2025-08-03",
93
+ "numeros": "45 41 02 14 23 46",
94
+ "boliyapa": 22,
95
+ "jackpot": 0.0,
96
+ "created_at": "2025-10-08 14:55:12"
97
+ },
98
+ {
99
+ "id_sorteo": 1218,
100
+ "fecha_sorteo": "2025-08-06",
101
+ "numeros": "38 23 06 21 33 36",
102
+ "boliyapa": 16,
103
+ "jackpot": 0.0,
104
+ "created_at": "2025-10-08 14:55:13"
105
+ },
106
+ {
107
+ "id_sorteo": 1219,
108
+ "fecha_sorteo": "2025-08-10",
109
+ "numeros": "24 46 29 19 25 47",
110
+ "boliyapa": 26,
111
+ "jackpot": 0.0,
112
+ "created_at": "2025-10-08 14:38:17"
113
+ },
114
+ {
115
+ "id_sorteo": 1220,
116
+ "fecha_sorteo": "2025-08-13",
117
+ "numeros": "36 07 43 32 20 33",
118
+ "boliyapa": 17,
119
+ "jackpot": 0.0,
120
+ "created_at": "2025-10-08 14:38:18"
121
+ },
122
+ {
123
+ "id_sorteo": 1221,
124
+ "fecha_sorteo": "2025-08-17",
125
+ "numeros": "34 11 39 07 26 27",
126
+ "boliyapa": 37,
127
+ "jackpot": 0.0,
128
+ "created_at": "2025-10-08 14:38:19"
129
+ },
130
+ {
131
+ "id_sorteo": 1222,
132
+ "fecha_sorteo": "2025-08-20",
133
+ "numeros": "28 37 09 29 06 49",
134
+ "boliyapa": 48,
135
+ "jackpot": 0.0,
136
+ "created_at": "2025-10-08 14:38:21"
137
+ },
138
+ {
139
+ "id_sorteo": 1223,
140
+ "fecha_sorteo": "2025-08-24",
141
+ "numeros": "12 37 16 08 22 17",
142
+ "boliyapa": 7,
143
+ "jackpot": 0.0,
144
+ "created_at": "2025-10-08 14:38:22"
145
+ },
146
+ {
147
+ "id_sorteo": 1224,
148
+ "fecha_sorteo": "2025-08-27",
149
+ "numeros": "40 19 13 46 30 36",
150
+ "boliyapa": 3,
151
+ "jackpot": 0.0,
152
+ "created_at": "2025-10-08 14:38:23"
153
+ },
154
+ {
155
+ "id_sorteo": 1225,
156
+ "fecha_sorteo": "2025-08-31",
157
+ "numeros": "30 35 32 12 38 04",
158
+ "boliyapa": 29,
159
+ "jackpot": 0.0,
160
+ "created_at": "2025-10-08 14:38:24"
161
+ },
162
+ {
163
+ "id_sorteo": 1226,
164
+ "fecha_sorteo": "2025-09-03",
165
+ "numeros": "23 21 15 14 48 18",
166
+ "boliyapa": 10,
167
+ "jackpot": 0.0,
168
+ "created_at": "2025-10-08 14:55:14"
169
+ },
170
+ {
171
+ "id_sorteo": 1227,
172
+ "fecha_sorteo": "2025-09-07",
173
+ "numeros": "29 15 07 33 17 23",
174
+ "boliyapa": 8,
175
+ "jackpot": 0.0,
176
+ "created_at": "2025-10-08 14:55:15"
177
+ },
178
+ {
179
+ "id_sorteo": 1228,
180
+ "fecha_sorteo": "2025-09-10",
181
+ "numeros": "01 16 06 45 33 38",
182
+ "boliyapa": 27,
183
+ "jackpot": 0.0,
184
+ "created_at": "2025-10-08 14:38:26"
185
+ },
186
+ {
187
+ "id_sorteo": 1229,
188
+ "fecha_sorteo": "2025-09-14",
189
+ "numeros": "35 06 12 33 38 02",
190
+ "boliyapa": 14,
191
+ "jackpot": 0.0,
192
+ "created_at": "2025-10-08 14:38:27"
193
+ },
194
+ {
195
+ "id_sorteo": 1230,
196
+ "fecha_sorteo": "2025-09-17",
197
+ "numeros": "39 44 19 34 24 14",
198
+ "boliyapa": 47,
199
+ "jackpot": 0.0,
200
+ "created_at": "2025-10-08 14:38:28"
201
+ },
202
+ {
203
+ "id_sorteo": 1231,
204
+ "fecha_sorteo": "2025-09-21",
205
+ "numeros": "46 44 02 28 20 32",
206
+ "boliyapa": 39,
207
+ "jackpot": 0.0,
208
+ "created_at": "2025-10-08 14:38:29"
209
+ },
210
+ {
211
+ "id_sorteo": 1232,
212
+ "fecha_sorteo": "2025-09-24",
213
+ "numeros": "21 39 43 46 31 24",
214
+ "boliyapa": 41,
215
+ "jackpot": 0.0,
216
+ "created_at": "2025-10-08 14:38:31"
217
+ },
218
+ {
219
+ "id_sorteo": 1233,
220
+ "fecha_sorteo": "2025-09-28",
221
+ "numeros": "24 17 31 09 35 44",
222
+ "boliyapa": 33,
223
+ "jackpot": 0.0,
224
+ "created_at": "2025-10-08 14:38:32"
225
+ },
226
+ {
227
+ "id_sorteo": 1234,
228
+ "fecha_sorteo": "2025-10-01",
229
+ "numeros": "13 36 37 41 14 10",
230
+ "boliyapa": 1,
231
+ "jackpot": 0.0,
232
+ "created_at": "2025-10-08 14:55:16"
233
+ },
234
+ {
235
+ "id_sorteo": 1235,
236
+ "fecha_sorteo": "2025-10-05",
237
+ "numeros": "02 31 06 14 45 23",
238
+ "boliyapa": 5,
239
+ "jackpot": 0.0,
240
+ "created_at": "2025-10-08 14:55:17"
241
+ }
242
+ ]
proyecto_tinka_ml/proyecto_tinka_ml/data/combinaciones_generadas.json ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "estrategia": "auto_ml",
4
+ "n": 1,
5
+ "ranked": [
6
+ {
7
+ "combo": [
8
+ 2,
9
+ 14,
10
+ 23,
11
+ 24,
12
+ 33,
13
+ 37
14
+ ],
15
+ "score": -107.88746739789008
16
+ }
17
+ ]
18
+ }
19
+ ]
proyecto_tinka_ml/proyecto_tinka_ml/data/probabilidades.json ADDED
@@ -0,0 +1,503 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "global": {
3
+ "1": {
4
+ "alpha": 8.0,
5
+ "beta": 52.0,
6
+ "p": 0.13333333333333333
7
+ },
8
+ "2": {
9
+ "alpha": 9.0,
10
+ "beta": 51.0,
11
+ "p": 0.15
12
+ },
13
+ "3": {
14
+ "alpha": 6.0,
15
+ "beta": 54.0,
16
+ "p": 0.1
17
+ },
18
+ "4": {
19
+ "alpha": 6.0,
20
+ "beta": 54.0,
21
+ "p": 0.1
22
+ },
23
+ "5": {
24
+ "alpha": 5.0,
25
+ "beta": 55.0,
26
+ "p": 0.08333333333333333
27
+ },
28
+ "6": {
29
+ "alpha": 10.0,
30
+ "beta": 50.0,
31
+ "p": 0.16666666666666666
32
+ },
33
+ "7": {
34
+ "alpha": 9.0,
35
+ "beta": 51.0,
36
+ "p": 0.15
37
+ },
38
+ "8": {
39
+ "alpha": 6.0,
40
+ "beta": 54.0,
41
+ "p": 0.1
42
+ },
43
+ "9": {
44
+ "alpha": 10.0,
45
+ "beta": 50.0,
46
+ "p": 0.16666666666666666
47
+ },
48
+ "10": {
49
+ "alpha": 6.0,
50
+ "beta": 54.0,
51
+ "p": 0.1
52
+ },
53
+ "11": {
54
+ "alpha": 7.0,
55
+ "beta": 53.0,
56
+ "p": 0.11666666666666667
57
+ },
58
+ "12": {
59
+ "alpha": 7.0,
60
+ "beta": 53.0,
61
+ "p": 0.11666666666666667
62
+ },
63
+ "13": {
64
+ "alpha": 7.0,
65
+ "beta": 53.0,
66
+ "p": 0.11666666666666667
67
+ },
68
+ "14": {
69
+ "alpha": 12.0,
70
+ "beta": 48.0,
71
+ "p": 0.2
72
+ },
73
+ "15": {
74
+ "alpha": 7.0,
75
+ "beta": 53.0,
76
+ "p": 0.11666666666666667
77
+ },
78
+ "16": {
79
+ "alpha": 7.0,
80
+ "beta": 53.0,
81
+ "p": 0.11666666666666667
82
+ },
83
+ "17": {
84
+ "alpha": 7.0,
85
+ "beta": 53.0,
86
+ "p": 0.11666666666666667
87
+ },
88
+ "18": {
89
+ "alpha": 5.0,
90
+ "beta": 55.0,
91
+ "p": 0.08333333333333333
92
+ },
93
+ "19": {
94
+ "alpha": 8.0,
95
+ "beta": 52.0,
96
+ "p": 0.13333333333333333
97
+ },
98
+ "20": {
99
+ "alpha": 7.0,
100
+ "beta": 53.0,
101
+ "p": 0.11666666666666667
102
+ },
103
+ "21": {
104
+ "alpha": 8.0,
105
+ "beta": 52.0,
106
+ "p": 0.13333333333333333
107
+ },
108
+ "22": {
109
+ "alpha": 8.0,
110
+ "beta": 52.0,
111
+ "p": 0.13333333333333333
112
+ },
113
+ "23": {
114
+ "alpha": 10.0,
115
+ "beta": 50.0,
116
+ "p": 0.16666666666666666
117
+ },
118
+ "24": {
119
+ "alpha": 12.0,
120
+ "beta": 48.0,
121
+ "p": 0.2
122
+ },
123
+ "25": {
124
+ "alpha": 6.0,
125
+ "beta": 54.0,
126
+ "p": 0.1
127
+ },
128
+ "26": {
129
+ "alpha": 5.0,
130
+ "beta": 55.0,
131
+ "p": 0.08333333333333333
132
+ },
133
+ "27": {
134
+ "alpha": 6.0,
135
+ "beta": 54.0,
136
+ "p": 0.1
137
+ },
138
+ "28": {
139
+ "alpha": 10.0,
140
+ "beta": 50.0,
141
+ "p": 0.16666666666666666
142
+ },
143
+ "29": {
144
+ "alpha": 9.0,
145
+ "beta": 51.0,
146
+ "p": 0.15
147
+ },
148
+ "30": {
149
+ "alpha": 7.0,
150
+ "beta": 53.0,
151
+ "p": 0.11666666666666667
152
+ },
153
+ "31": {
154
+ "alpha": 8.0,
155
+ "beta": 52.0,
156
+ "p": 0.13333333333333333
157
+ },
158
+ "32": {
159
+ "alpha": 10.0,
160
+ "beta": 50.0,
161
+ "p": 0.16666666666666666
162
+ },
163
+ "33": {
164
+ "alpha": 9.0,
165
+ "beta": 51.0,
166
+ "p": 0.15
167
+ },
168
+ "34": {
169
+ "alpha": 7.0,
170
+ "beta": 53.0,
171
+ "p": 0.11666666666666667
172
+ },
173
+ "35": {
174
+ "alpha": 8.0,
175
+ "beta": 52.0,
176
+ "p": 0.13333333333333333
177
+ },
178
+ "36": {
179
+ "alpha": 10.0,
180
+ "beta": 50.0,
181
+ "p": 0.16666666666666666
182
+ },
183
+ "37": {
184
+ "alpha": 9.0,
185
+ "beta": 51.0,
186
+ "p": 0.15
187
+ },
188
+ "38": {
189
+ "alpha": 10.0,
190
+ "beta": 50.0,
191
+ "p": 0.16666666666666666
192
+ },
193
+ "39": {
194
+ "alpha": 7.0,
195
+ "beta": 53.0,
196
+ "p": 0.11666666666666667
197
+ },
198
+ "40": {
199
+ "alpha": 5.0,
200
+ "beta": 55.0,
201
+ "p": 0.08333333333333333
202
+ },
203
+ "41": {
204
+ "alpha": 6.0,
205
+ "beta": 54.0,
206
+ "p": 0.1
207
+ },
208
+ "42": {
209
+ "alpha": 6.0,
210
+ "beta": 54.0,
211
+ "p": 0.1
212
+ },
213
+ "43": {
214
+ "alpha": 9.0,
215
+ "beta": 51.0,
216
+ "p": 0.15
217
+ },
218
+ "44": {
219
+ "alpha": 7.0,
220
+ "beta": 53.0,
221
+ "p": 0.11666666666666667
222
+ },
223
+ "45": {
224
+ "alpha": 8.0,
225
+ "beta": 52.0,
226
+ "p": 0.13333333333333333
227
+ }
228
+ },
229
+ "recent": {
230
+ "1": {
231
+ "alpha": 5.150367628938792,
232
+ "beta": 34.56371252205608,
233
+ "p": 0.1296861870993069
234
+ },
235
+ "2": {
236
+ "alpha": 6.392834501328441,
237
+ "beta": 33.32124564966642,
238
+ "p": 0.16097148610826623
239
+ },
240
+ "3": {
241
+ "alpha": 3.465809423819209,
242
+ "beta": 36.24827072717566,
243
+ "p": 0.08726903432339445
244
+ },
245
+ "4": {
246
+ "alpha": 3.6284088465513236,
247
+ "beta": 36.085671304443544,
248
+ "p": 0.09136328558425466
249
+ },
250
+ "5": {
251
+ "alpha": 2.737134608645551,
252
+ "beta": 36.97694554234931,
253
+ "p": 0.0689210123522648
254
+ },
255
+ "6": {
256
+ "alpha": 7.189970645879335,
257
+ "beta": 32.52410950511553,
258
+ "p": 0.18104336342533217
259
+ },
260
+ "7": {
261
+ "alpha": 5.94562565228181,
262
+ "beta": 33.768454498713055,
263
+ "p": 0.14971077334981073
264
+ },
265
+ "8": {
266
+ "alpha": 3.5157090897555836,
267
+ "beta": 36.198371061239285,
268
+ "p": 0.08852550723543606
269
+ },
270
+ "9": {
271
+ "alpha": 6.758123145934322,
272
+ "beta": 32.95595700506055,
273
+ "p": 0.17016944922907967
274
+ },
275
+ "10": {
276
+ "alpha": 3.6740036135632312,
277
+ "beta": 36.04007653743164,
278
+ "p": 0.09251136119971784
279
+ },
280
+ "11": {
281
+ "alpha": 4.247949109102931,
282
+ "beta": 35.466131041891934,
283
+ "p": 0.10696330099934385
284
+ },
285
+ "12": {
286
+ "alpha": 4.637483526283527,
287
+ "beta": 35.07659662471134,
288
+ "p": 0.11677177234501185
289
+ },
290
+ "13": {
291
+ "alpha": 4.592222765248582,
292
+ "beta": 35.12185738574628,
293
+ "p": 0.1156321069955222
294
+ },
295
+ "14": {
296
+ "alpha": 8.635239791619453,
297
+ "beta": 31.078840359375413,
298
+ "p": 0.21743522092889603
299
+ },
300
+ "15": {
301
+ "alpha": 4.475099900393831,
302
+ "beta": 35.23898025060104,
303
+ "p": 0.11268295484571927
304
+ },
305
+ "16": {
306
+ "alpha": 4.461371248866236,
307
+ "beta": 35.25270890212863,
308
+ "p": 0.11233726758630402
309
+ },
310
+ "17": {
311
+ "alpha": 4.7144253307027855,
312
+ "beta": 34.99965482029208,
313
+ "p": 0.11870916593757959
314
+ },
315
+ "18": {
316
+ "alpha": 2.882702996290655,
317
+ "beta": 36.831377154704214,
318
+ "p": 0.0725864223804373
319
+ },
320
+ "19": {
321
+ "alpha": 5.3298029142097345,
322
+ "beta": 34.38427723678513,
323
+ "p": 0.13420436515073658
324
+ },
325
+ "20": {
326
+ "alpha": 4.455681876257035,
327
+ "beta": 35.25839827473783,
328
+ "p": 0.1121940092611063
329
+ },
330
+ "21": {
331
+ "alpha": 5.389866710734496,
332
+ "beta": 34.32421344026037,
333
+ "p": 0.13571677073325028
334
+ },
335
+ "22": {
336
+ "alpha": 4.999458359063988,
337
+ "beta": 34.71462179193088,
338
+ "p": 0.12588629372896978
339
+ },
340
+ "23": {
341
+ "alpha": 7.025236122466341,
342
+ "beta": 32.68884402852853,
343
+ "p": 0.17689535035826212
344
+ },
345
+ "24": {
346
+ "alpha": 8.566584631168398,
347
+ "beta": 31.14749551982647,
348
+ "p": 0.21570648491914773
349
+ },
350
+ "25": {
351
+ "alpha": 3.5695074682336285,
352
+ "beta": 36.144572682761236,
353
+ "p": 0.08988014967644188
354
+ },
355
+ "26": {
356
+ "alpha": 2.8235910172675736,
357
+ "beta": 36.89048913372729,
358
+ "p": 0.07109798354971696
359
+ },
360
+ "27": {
361
+ "alpha": 3.49255479466063,
362
+ "beta": 36.22152535633424,
363
+ "p": 0.08794248239873029
364
+ },
365
+ "28": {
366
+ "alpha": 6.681595455699942,
367
+ "beta": 33.032484695294926,
368
+ "p": 0.16824248302607517
369
+ },
370
+ "29": {
371
+ "alpha": 5.9163256101910395,
372
+ "beta": 33.79775454080383,
373
+ "p": 0.14897299868703698
374
+ },
375
+ "30": {
376
+ "alpha": 4.398079777126934,
377
+ "beta": 35.316000373867936,
378
+ "p": 0.11074358918562938
379
+ },
380
+ "31": {
381
+ "alpha": 5.639025847924097,
382
+ "beta": 34.07505430307077,
383
+ "p": 0.14199059443109965
384
+ },
385
+ "32": {
386
+ "alpha": 6.711911303179578,
387
+ "beta": 33.00216884781529,
388
+ "p": 0.16900583565477442
389
+ },
390
+ "33": {
391
+ "alpha": 6.325025585089622,
392
+ "beta": 33.38905456590524,
393
+ "p": 0.1592640585163138
394
+ },
395
+ "34": {
396
+ "alpha": 4.51448229205958,
397
+ "beta": 35.19959785893529,
398
+ "p": 0.1136746029341558
399
+ },
400
+ "35": {
401
+ "alpha": 5.470499942519833,
402
+ "beta": 34.24358020847504,
403
+ "p": 0.1377471144168699
404
+ },
405
+ "36": {
406
+ "alpha": 6.931936391066081,
407
+ "beta": 32.782143759928786,
408
+ "p": 0.17454606438599413
409
+ },
410
+ "37": {
411
+ "alpha": 6.073354358668248,
412
+ "beta": 33.64072579232662,
413
+ "p": 0.152926980445652
414
+ },
415
+ "38": {
416
+ "alpha": 6.874240082502508,
417
+ "beta": 32.83984006849236,
418
+ "p": 0.17309327211825914
419
+ },
420
+ "39": {
421
+ "alpha": 4.715888128129645,
422
+ "beta": 34.998192022865226,
423
+ "p": 0.11874599915696418
424
+ },
425
+ "40": {
426
+ "alpha": 2.858565436437754,
427
+ "beta": 36.85551471455712,
428
+ "p": 0.07197863895045153
429
+ },
430
+ "41": {
431
+ "alpha": 3.765397284153859,
432
+ "beta": 35.94868286684101,
433
+ "p": 0.09481265258663012
434
+ },
435
+ "42": {
436
+ "alpha": 3.4442413898320985,
437
+ "beta": 36.269838761162774,
438
+ "p": 0.08672595151988727
439
+ },
440
+ "43": {
441
+ "alpha": 6.00435635465089,
442
+ "beta": 33.70972379634398,
443
+ "p": 0.15118961163955036
444
+ },
445
+ "44": {
446
+ "alpha": 4.8517455856746885,
447
+ "beta": 34.86233456532018,
448
+ "p": 0.12216688809681893
449
+ },
450
+ "45": {
451
+ "alpha": 5.423818343623212,
452
+ "beta": 34.29026180737166,
453
+ "p": 0.13657167238927834
454
+ }
455
+ },
456
+ "blend": {
457
+ "1": 0.1322391894631254,
458
+ "2": 0.15329144583247986,
459
+ "3": 0.09618071029701833,
460
+ "4": 0.0974089856752764,
461
+ "5": 0.07900963703901277,
462
+ "6": 0.1709796756942663,
463
+ "7": 0.14991323200494322,
464
+ "8": 0.09655765217063081,
465
+ "9": 0.16771750143539055,
466
+ "10": 0.09775340835991535,
467
+ "11": 0.11375565696646982,
468
+ "12": 0.11669819837017022,
469
+ "13": 0.11635629876532333,
470
+ "14": 0.20523056627866879,
471
+ "15": 0.11547155312038244,
472
+ "16": 0.11536784694255786,
473
+ "17": 0.11727941644794054,
474
+ "18": 0.08010926004746452,
475
+ "19": 0.1335946428785543,
476
+ "20": 0.11532486944499856,
477
+ "21": 0.1340483645533084,
478
+ "22": 0.13109922145202427,
479
+ "23": 0.1697352717741453,
480
+ "24": 0.20471194547574428,
481
+ "25": 0.09696404490293256,
482
+ "26": 0.07966272839824841,
483
+ "27": 0.09638274471961908,
484
+ "28": 0.1671394115744892,
485
+ "29": 0.1496918996061111,
486
+ "30": 0.11488974342235547,
487
+ "31": 0.13593051166266323,
488
+ "32": 0.167368417363099,
489
+ "33": 0.15277921755489415,
490
+ "34": 0.1157690475469134,
491
+ "35": 0.1346574676583943,
492
+ "36": 0.1690304859824649,
493
+ "37": 0.1508780941336956,
494
+ "38": 0.1685946483021444,
495
+ "39": 0.11729046641375593,
496
+ "40": 0.07992692501846879,
497
+ "41": 0.09844379577598902,
498
+ "42": 0.09601778545596618,
499
+ "43": 0.1503568834918651,
500
+ "44": 0.11831673309571233,
501
+ "45": 0.1343048350501168
502
+ }
503
+ }
proyecto_tinka_ml/proyecto_tinka_ml/data/reporte.html ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="es">
3
+ <head>
4
+ <meta charset="utf-8"/>
5
+ <title>Reporte Tinka</title>
6
+ <style>
7
+ body { font-family: Arial, Helvetica, sans-serif; margin: 20px; }
8
+ h1, h2 { margin: 0.4em 0; }
9
+ pre { background: #f6f8fa; padding: 12px; overflow: auto; }
10
+ .small { color:#555; font-size: 0.9em; }
11
+ </style>
12
+ </head>
13
+ <body>
14
+ <h1>Reporte de Análisis — Tinka</h1>
15
+ <p class="small">Este reporte se genera a partir de los resultados históricos presentes en tu base de datos.</p>
16
+
17
+ <h2>Frecuencias</h2>
18
+ <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABdwAAAJYCAYAAAB4syQkAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjMsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvZiW1igAAAAlwSFlzAAAXEgAAFxIBZ5/SUgAAV5BJREFUeJzt3QeYVOX5P+6HDgKKYEFFQREUMSoaK0GIiUqComAvscWusSS22GtirLFFjRpbbJHYO/aCImAsQUVFEDWoKFgogsL8r/d8/7O/BXapZ2fZ2fu+rnGGc86c885p4Gfeed4GhUKhEAAAAAAAwGJpuHhvBwAAAAAAEoE7AAAAAADkQOAOAAAAAAA5ELgDAAAAAEAOBO4AAAAAAJADgTsAAAAAAORA4A4AAAAAADkQuAMAAAAAQA4E7gAAAAAAkAOBOwAAAAAA5EDgDgAAAAAAORC4AwAAAABADgTuAAAAAACQA4E7AAAAAADkQOAOAAAAAAA5ELgDAAAAAEAOBO4AAAAAAJADgTsAAAAAAORA4A4AAAAAADkQuAMAAAAAQA4E7gAAAAAAkIPGeawEAACgvhsyZEg88cQT0bJlyzj22GOjcWP/uwUAUN/4FyAAAMBi+uSTT2KHHXaISZMmxf333y9sBwCop5SUAQCgwtixY6NBgwbZI72uK/r06ZO1+cwzz4y64Kabbsra26lTp6iv0rFK+yAdu7ruhx9+iF133TW+/PLLuPrqq6Nfv3613aR67frrr8/OrcMOOyzqm3HjxkWzZs1izTXXjBkzZtR2cwCgXhK4AwA1EqItyAPI33333Zddh+mZ0jjhhBPi5ZdfjtNPPz0OOuig2m5OvTZ58uQ47bTTstD5lFNOmWv+119/nf0CIR2r7bbbLlZaaaWKv5PSF2E16dBDD63Y1ry+bFvQv0c/+OCDud672mqrxf777x+jR4+Oq666qkY/DwBQNb9zBABqzIorrljbTWAhNWnSJNZaa62K19Q9KWi/+eabY999940dd9yxtptT9u65557461//GgcccECcddZZtd2ceu/CCy+Mzz77LI444ojo0KFDlddHCqRL7Zlnnom///3vC/WedA9u27ZttfOrK1t08sknxz/+8Y8499xzs8/apk2bhW4vALDoBO4AQI1JoQd1yyqrrBLvvvtubTcD6oyBAwdGoVCo7WYQEdOmTYvLL788ez2vcjLt27ePHj16xIYbbhgbbbRRdgxr0tSpU7NfPqSAfP3114/hw4cv0Pu22GKLePbZZxd6e6mX+69//eusJ38qr3PcccctQqsBgEWlpAwAAAB13l133ZWVjNlggw2ie/fuVS7zm9/8JsaPHx+PPPJI1gN8wIABNd6uVNomlXhJpYeqa1fe9t577+w59ar3hRAAlJbAHQBYogbpTKHEwQcfHKuvvnpWg3fOOrezZs2K2267Leu9l0rWNG3aNJZffvnYZptt4o477phvsPDOO+9kpQbWWWedaN26dbRq1SorobL77rvHv//972z9Raln4YLUmy8uU11PxEVtc/rsxbrCafC7VCoh9Y5s2bJlLLPMMrHVVlvFY489FvMzdOjQrKxAGkRvqaWWiqWXXjr7/KkExuOPP75Qg6a+8sorceKJJ0avXr2iY8eO0bx586xcwWabbRZ/+ctfsvrJi2rMmDHZOvr27Rtdu3bNPmc6PqmtxxxzTDYY4IJI++r888+P9dZbL1vHsssuG1tvvXU8+uij1b7nxx9/zIKpNIDncsstl5VyaNeuXXZu7LbbbnHDDTdU+9503HfZZZfs1wHpnE3v/8UvfhE33nhjzJw5c6H3w3777Zft//S8MIOuFs/XVE4mSc9z1nyufI6mX6BcccUVscMOO0S3bt2yc6pFixbZeXLggQfGyJEjY3GlfZ72fTpH0rFM5+8FF1yQDTK6INI5mI59CinT+9P5u/baa8fRRx+9wOfD/PbdiBEjsgFPUy3vdPzWWGON+P3vfx+TJk3K9fhU9/607Oabb57t/3Su/vKXv4znn39+tnMzHafUEztdu2m5dC957bXX5vk587jvpOs51Tr/yU9+kt0v57wvpPM7lS5J96J03qf9l66DdD3Mr2d2Csd/9atfZW1L11s6R7p06RL9+/fPao9///33sbCuu+667HnPPfesdplGjRpFKaV7Zup1n+5pp556asm2u/3222fH7P3331+kXvIAwGIoAADk6IwzzkgpTvZYEGPGjKlY/rbbbiu0atUqe73UUksVWrZsWejYsWPFsl999VVhyy23rFg+PZZZZpnZ/ty/f//C9OnTq9zW+eefX2jYsGHFss2bNy+0bdt2tmmTJk2qWP6ZZ55ZoM9SXCYtP6fFaXP67Gn+FVdcUdh0002z102aNKnYR+nRoEGDwg033FBlu3788cfCUUcdNdu20j5ddtlls/cV21Ld8Uivq/usxWOU1lV52jrrrFP4/PPPC4uid+/eFetp2rRpoV27drMdm9TWF154YZ7v/eMf/1jo1atX9rpx48aFNm3azNa+dH5WtZ+23nrruY5Rs2bNZptWlWOPPXa2Y5G216hRo4ppW221VeHbb7+d63033nhjNr/y+V207777ZvPSc3Wqev9LL71UWHHFFbPzunh+pz9XfqRl5txOcV+layE9F6elzz9o0KBCHveC9Ej7prj+dE2kY5Vep2NXlX/+85+zHYP0ukWLFhV/bt26deHxxx9f6HZV3nfpnpOuqeIxr3y+de/evfDdd9/ldnyqen/xddov6fNUPh4PPvhg4fvvvy9ss802FddEun4rX3/Dhw+vcvt53HcuuuiiQteuXSu2XbyWiveFr7/+utCnT5+K9aXzPi1TvLekx3HHHVdl+/bff//Z2pLuaenzVJ5W1f1nXlJ7isfv5ZdfXqj3FreZjlue0vHr1q1btk+effbZbFrxmFd1bsx57VR3bSyoX/ziF9l6TjjhhMVaDwCwcATuAMASE7in0CUFy8OGDauYP2rUqIpQtBiqbrDBBlkYNWXKlGze5MmTCzfffHNhhRVWyOYfc8wxc23nb3/722xB03/+85+KeWk9TzzxRGG33XYrfPPNN7kF7ovb5mLwlULtVVZZpXDfffcVZsyYkc179913C5tttlnFfkth05xSyFJs2wEHHFCxL5O0fFpf+szVHY+qAq/tt9++cNdddxXGjx9fMW3q1KmFe+65p7DWWmtl7xswYEBhURx99NGFq666qvDee+8VZs6cmU374YcfCkOHDi307ds3W/fKK6+cbW9Oxf1cDMqvueaawrRp07J548aNK+y8884Vn+v++++f7b233nprRUB9/fXXV4Sss2bNyr48SJ8tvX9O6YuQ4joPPvjgin2Sju2ll15aES7PuY9rKnBfmPcn55xzTuHCCy8svPXWW9l+TtJ+/+9//1vYa6+9Kr6g+fTTTwsLK+3j4r7ZZZddsmOQpGOXjnHlALeqUDFdjyk8TfswncfpXEzHIz3SuZ/Wmd679NJLFz766KOFaltx36WAN50rBx54YEX70vV55ZVXVoTwp512Wo0dn/T50xcI1157bcU5nT7bRhttlM3v1KlT4cgjj8y+CPnXv/6VXfvp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AAAAADkQuAMAAAAAQA4E7gAAAAAAkAOBOwAAAAAA5EDgDgAAAAAAOWicx0rqq/bt28eUKVNitdVWq+2mAAAAAAAwH+PGjYuWLVvGZ599FjVBD/fFkML2H374obabAQAAAADAAkh5bsp1a4oe7ouh2LN95MiRtd0UAAAAAADmo3v37lGT9HAHAAAAAIAcCNwBAAAAACAHAncAAAAAAMiBwB0AAAAAAHIgcAcAAAAAgBwI3AEAAAAAIAcCdwAAAAAAyIHAHQAAAAAAciBwBwAAAACAHAjcAQAAAAAgBwJ3AAAAAADIgcAdAAAAAAByUNaB+7Bhw2LXXXeNlVdeOZo0aRJt2rSJXr16xY033hiFQqG2mwcAAAAAQBlpHGXq3//+d+y2224xc+bM2HDDDbOgfcKECfHCCy/Eiy++GE8++WTcdttttd1MAAAAAADKRFn2cP/xxx/j8MMPz8L2FKqPGDEi7rrrrnj66afjzTffjLZt28btt98ezzzzTG03FQAAAACAMlGWgfu7774bX3zxRay11lqx5557zjavW7dusffee1eUnAEAAAAAgDyUZeDerFmzBVquXbt2Nd4WAAAAAADqh7IM3NdYY43o3LlzjBo1KisdU9k777wT//znP2PZZZeNAQMG1FobAQAAAAAoL2UZuDdq1ChuvvnmaNOmTey1116x0UYbxe677x5bbbVVrLfeetGhQ4d46qmnslruAAAAAACQh8ZRpnr27BnPPfdc1ov9tddeyx5J06ZNY+utt856wS+o7t27Vzl99OjRWU96AAAAAAAo28D9jjvuiP333z8222yz7HUKzf/3v//FRRddFBdffHE888wzMWTIkAWu9w4ANaHTSQ/nvs6x5/fLfZ1AzXM/AACAuq8sA/f3338/9t1331hhhRXioYceilatWmXTu3TpEtdee20WvKfp//jHP+Kwww6b7/pGjhy5UD3fAQAAAACof8qyhvudd94ZP/zwQ/Tt27cibK9s1113zZ6ff/75WmgdAAAAAADlqCwD908++SR7XmaZZaqcX5w+adKkkrYLAAAAAIDyVZaBe/v27bPn4cOHVzl/2LBh2XOnTp1K2i4AAAAAAMpXWQbuO+ywQ0XJmKuvvnq2ea+88kpceuml2eudd965VtoHAAAAAED5KcvAfcMNN4zjjjsue3344YfHuuuum9Vt/9nPfhY9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nXeyP/ft2zc6duwYV199dWy66aax0047xcYbbxzff/99/Pa3v12sbW255ZZx6623xujRo2Pq1KnZdtPgrI0bN47TTz89LrvssgVaT/fu3at8pPUCAAAAAECt9HDfZ599YplllolZs2ZV1HB/4IEHYtddd41hw4Zlj1T65cADD4yjjz56sbZ19tlnz/bnVE7m5JNPjp/+9Kex7bbbxplnnhkHH3xwtGjRYrG2Q83qdNLDua9z7Pn9cl8nNct5AADUB/7NU/+OkeOTL8eHUnLPXjT2G+Wu5IF7+/bt45BDDpltWqrdnnq8v/vuuzFp0qSs7EyxtntN2GabbbLQffjw4TF06NDo06fPPJcfOXJkldNTL3cAAAAAAKiVwH1e1l577ZJtq0uXLlngPn78+JJtEwAAAACA8lXyGu5LitSTPmnZsmVtNwUAAAAAgDJQ4z3cUx31Bg0axBFHHBFt27adq676vKT3nXbaabm3acKECfHCCy9krzfccMPc1w8AAAAAQP1T44F7Gpg0Bee77bZbFrgX/1woFGo0cB8yZEh88cUXsf3220ejRo0qpo8dOzb23nvvmDJlSvTv3z86dOiwSOsHAAAAAICSBu433nhj9rzSSivN9uea9t5778X++++fDdKaerG3adMmPvrooxgxYkR8//332YCn1113XUnaAgAAAABA+avxwH3fffed559ryqabbhqHHXZYDB06NIYNG5bVbE/12jfYYIPYZZddsnktWrQoSVsAAAAAACh/NR6415Zu3brF3/72t9puBgAAAAAA9UTDUm/w888/jwceeCDGjBlT7TJpXlom1WAHAAAAAIC6oOSB+yWXXBIDBgzI6qhXZ9q0adkyl112WUnbBgAAAAAAdSZwf/TRR7MBS1PJl+qss8462TIPP/xwSdsGAAAAAAB1JnD/6KOPomvXrvNdrkuXLjFu3LiStAkAAAAAAOpc4D5z5swFWq5BgwYxffr0Gm8PAAAAAADUycB9jTXWiJdffjl+/PHHapdJ89Iyq622WknbBgAAAAAAdSZw33777eOzzz6Lk046KQqFQpXL/PGPf8yW6d+/f6mbBwAAAAAAi6RxlNgf/vCHuOWWW+LSSy+NwYMHx29/+9vo3LlzNm/06NFxww03xH//+99o3759HH/88aVuHgAAAAAA1I3AvW3btvHEE0/EgAED4q233opjjz12tvmp13saVPXf//53LLfccqVuHgAAAAAA1I3APenWrVuMHDky7rnnnnjyySfj448/zqavuuqq8ctf/jIGDhwYjRo1qo2mAQAAAABA3QnckxSo77LLLtkDAAAAAADqupIPmgoAAAAAAOWo1nq4T506NYYPHx7jx4+P6dOnV7vcPvvsU9J2AQAAAABAnQncTz/99Lj00kuz0L06afDUBg0aCNwBAAAAAKgTSh64X3DBBXHuuedmNdz79esXXbt2jdatW5e6GQAAAAAAULcD9+uuuy5atGgRL7zwQmy44Yal3jwAAAAAAJTHoKkff/xx9O7dW9gOAAAAAEBZKXng3r59+2jZsmWpNwsAAAAAAOUVuO++++7x7LPPxpQpU0q9aQAAAAAAKJ/A/cwzz4xu3bpF//7944MPPij15gEAAAAAoDwGTf31r38ds2bNynq5p+C9Y8eO0aFDh2jYcO7sv0GDBvHUU0+VuokAAAAAALDkB+4paC+aOXNmfPjhh9mjKilwBwAAAACAuqDkgfuYMWNKvUkAAAAAACi/wD2VkAEAAAAAgHJT8kFTAQAAAACgHNVa4P7222/HscceGz179oy11lorTjjhhIp5Q4YMicsvvzwmTpxYW80DAAAAAIAlu6RMcskll8RJJ50UP/74Y8XgqF9++eVsy6QwvlmzZnHIIYfURhMBAAAAAGDJ7uH+8MMPx3HHHRerrrpq3HPPPfHFF19EoVCYbZktttgill9++bj//vtL3TwAAAAAAKgbPdxT7/aWLVvG4MGDY4011qh2uQ022CBGjRpV0rYBAAAAAECd6eE+YsSI2GyzzeYZtifLLbdcfPbZZyVrFwAAAAAA1KnAfcaMGdG6dev5LpdKzTRuXCsl5gEAAAAAYMkP3FdfffV444035hvKv/nmm9G1a9eStQsAAAAAAOpU4N6/f/8YO3ZsVsu9OhdccEFMmDAhBg4cWNK2AQAAAADAoip5zZYTTjghbrvttjj++ONj6NChMWDAgGz6559/Hvfee2/2SPNTT/gjjzyy1M0DAAAAAIC6Ebgvu+yy8eSTT8bOO+8cd999dwwaNCib/thjj2WPQqEQ66yzTtx3330LVOsdAAAAAACWBLUyKmmqzf7666/Hgw8+GE888URWYmbWrFnRoUOH2HrrrWOnnXaKRo0a1UbTAAAAAACg7gTuScOGDWOHHXbIHgAAAAAAUNeVfNDU2vLVV1/FCiusEA0aNIg111yztpsDAAAAAECZKXkP9+eff36hlt9yyy1z2e4f/vCH+PLLL3NZFwAAAAAA1Hrg3qdPn6yX+YKaOXPmYm/zqaeeiptvvjkOPvjg+Pvf/77Y6wMAAAAAgFoP3PfZZ58qA/c0aOrHH38cr732Wnz77bdZbfc2bdos9vamTZsWhxxySKyzzjpx3HHHCdwBAAAAACiPwP2mm26a5/xJkybFQQcdFP/973/j5ZdfXuztnXXWWfHhhx/Gc889F02aNFns9QEAAAAAQJ0YNHXZZZeNW265Jb755pv44x//uFjrevPNN+Piiy+O/fffP3r16pVbGwEAAAAAYIkP3JOllloqNtlkk3jggQcWeR2pRM2BBx6YlaW54IILcm0fAAAAAADUekmZBTV58uSsvMyiuuKKK2LYsGFx4403Rrt27RarLd27d69y+ujRo6Nz586LtW4AAAAAAMrDEhm4P/jgg/H8889nA50uinHjxsWpp54avXv3jv322y/39vF/Op30cO7rHHt+v9zXCQBLunL7O7VUn6fc9huUI9dp/TtGjg/UXe7ZUEcD9wMOOGCevdrfe++9eOutt6JQKMQf/vCHRdrGEUccETNmzIhrrrkm8jBy5MiF6vkOAAAAAED9U/LA/aabbprvMquttlqcccYZsc8++yzSNh566KGsdvuhhx462/Tvv/8+e/7000+jT58+2es777wz2rdvv0jbAQAAAACAWgvcn3nmmWrnNW3aNFZaaaXo1KnTYm/n66+/jueee67KeSl4L84rhvAAAAAAAFCnAvdUV72mpXI0VRk7dmysvvrq2UCnH3zwQY23AwAAAACA+qNhbTcAAAAAAADKQckD96effjoGDhwYL7zwQrXLPP/889ky6RkAAAAAAOqCkgfu1157bQwePDg22GCDapdJ85544om45pprSto2AAAAAACoMzXcX3311ejRo0e0bt262mWWXnrp2HDDDWPo0KG5bjsNxlpdfXcAAAAAAKhTPdw/++yzWHXVVee7XFpm/PjxJWkTAAAAAADUucC9ZcuW8fnnn893uS+++CKaN29ekjYBAAAAAECdC9xTOZmXXnopxo0bV+0yaV4aVHX99dcvadsAAAAAAKDOBO4HHHBATJ8+PbbbbrsYPnz4XPPTtO233z5++OGHbFkAAAAAAKgLSj5o6h577BH33ntvDBo0KDbddNOsF3vnzp2zeaNHj4433ngjG9h0wIAB8Zvf/KbUzQMAAAAAgLoRuCd33nln/OlPf4pLLrkkXn/99exR1KZNmzj22GPj5JNPro2mAQAAAABA3QncGzZsGKeeemqceOKJWQmZjz/+OJu+6qqrxkYbbRRNmzatjWYBAAAAAEDdCtyLmjRpEptvvnn2AAAAAACAuqxWA/eJEyfGiBEj4ssvv4yOHTvGFltsUZvNAQAAAACARdYwasGECRNizz33jPbt20ffvn1j7733juuvv75ifnrdtm3bePHFF2ujeQAAAAAAsOQH7qlXe+rJngZOXXfddePwww+PQqEw2zIDBw6M7777LgYNGlTq5gEAAAAAQN0I3M8777wYPXp0nH766fHaa6/FFVdcMdcyqXf7euutF88991ypmwcAAAAAAHUjcL/vvvuia9euceaZZ85zuc6dO8enn35asnYBAAAAAECdCtxTiL7++uvPd7kGDRrEt99+W5I2AQAAAABAnQvcl1566Rg/fvx8l0tlZ5ZffvmStAkAAAAAAOpc4L7xxhvHsGHDYsyYMdUu88Ybb8Trr78ePXv2LGnbAAAAAACgzgTuv/vd72L69OkxYMCAeOedd+aa/8EHH8RvfvObKBQKceSRR5a6eQAAAAAAUDcC9759+8YJJ5wQb775Zqy77rqx9tprZ/XaH3/88ay2e7du3eK///1vnHzyyfGzn/2s1M0DAAAAAIC6Ebgn559/ftx1113xk5/8JN57772sN3uq6/7WW29Fly5d4rbbbotzzjmnNpoGAAAAAACLpHGU2Lfffpv1aN9ll12yx4QJE2Ls2LExa9as6NChQ6yyyiqlbhIAAAAAANS9wL1Nmzax6aabxssvv5z9efnll88eAAAAAABQl5W8pMwyyywTa6yxRqk3CwAAAAAA5RW49+jRI0aPHl3qzQIAAAAAQHkF7ieeeGIMGzYsBg0aVOpNAwAAAABA+dRwb9GiRRx44IGx2267xXbbbRfbb799rLbaatG8efMql99yyy1L3UQAAAAAAFjyA/c+ffpEgwYNolAoxIMPPhgPPfTQPJefOXNmydoGAAAAAAB1JnDfZ599ssAdAAAAAADKSckD95tuuqnUmwQAAAAAgLo/aOpWW20VF1xwQZXzxo0bFxMnTqzpJgAAAAAAQN0P3J999tl49913q5y3+uqrx/HHH1/TTQAAAAAAgLofuM9LGjg1PQAAAAAAoK6r1cAdAAAAAADKhcAdAAAAAAByIHAHAAAAAIAcCNwBAAAAAKCuBO4333xzNGrUaK5HgwYNqp2XHo0bNy5F8wAAAAAAYLGVJNEuFAolfR8AAAAAAJRd4D5r1qya3gQAAAAAANS6sq7hfskll8TAgQOjS5cuscwyy0SzZs2iY8eOsc8++8Rbb71V280DAAAAAKCMlHXg/qc//SkeffTRaNu2bfziF7+Ifv36RfPmzePWW2+NjTbaKB566KHabiIAAAAAAGWirEclvf/++7NgPYXslf3tb3+LI444Ig488MD45JNPDM4KAAAAAMBiK+se7j179pwrbE8OP/zw6Ny5c3z++efx9ttv10rbAAAAAAAoL2UduM9LkyZNsuemTZvWdlMAAAAAACgD9TJwTzXcR40alQ2mmh4AAAAAALC46kXx8gsvvDBGjhwZU6ZMiXfeeSd7vfLKK8cdd9wRjRo1qu3mAQAAAABQBupF4P7444/HU089VfHnjh07xi233JINqLogunfvXuX00aNHZ7XgAQAAAACgXgTuTz75ZPb89ddfx1tvvRVnn3129O7dO84999w45ZRTart5AGWh00kP57q+sef3K8l2qttWqbZTKuW238rt+JSK/QYUuR8s+Ur1byuWbK5VEucB1C31InAvatOmTfTq1SseeeSR2HzzzeO0006LbbbZJjbeeON5vi+VoFmYnu8AAAAAANQ/9XLQ1CZNmsRuu+0WhUIhHnzwwdpuDgAAAAAAZaBeBu7Jcsstlz1PmDChtpsCAAAAAEAZqLeB+3PPPZc9G/QUAAAAAIA8lG3g/tJLL8Vjjz0Ws2bNmm36Dz/8EFdccUXceuut0aJFi6y0DAAAAAAALK6yHTT1/fffj/333z8rHbPRRhtFu3bt4ssvv4y33norxo8fH82bN4+bbropVl111dpuKgAAAAAAZaBsA/fevXvHySefnJWOefPNN7OwvWnTptGpU6fYeeed46ijjoo111yztpsJAAAAAECZKNvAffXVV4/zzjuvtpsBAAAAAEA9UbY13AEAAAAAoJQE7gAAAAAAkAOBOwAAAAAA5EDgDgAAAAAAORC4AwAAAABADgTuAAAAAACQA4E7AAAAAADkQOAOAAAA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gBwI3AEAAAAAIAcCdwAAAAAAyIHAHQAAAAAAciBwr2emTZsWp59+enTt2jWaN28eK6+8chxwwAHx6aef1nbTgBowYsSIOP/882PgwIHRoUOHaNCgQfaYn5tuuik22WSTaNWqVbRt2zZ+/etfx5AhQ0rSZqBmTJ06Ne6777747W9/G2uttVb274CWLVvG+uuvH2effXZMnjy52ve6J0D5ueSSS7J/H3Tp0iWWWWaZaNasWXTs2DH22WefeOutt6p9n/sBlL+vvvoqVlhhhez/G9Zcc815LuueAOWnT58+FdlBVY/HHnusyve5H/w/DQqFQqHSnylj33//ffz85z+PV155JVZaaaXo1atXjB07Nl599dVYfvnls+lrrLFGbTcTyNGOO+4Y999//1zT53XrP+aYY+Kyyy6LFi1axDbbbJPdO5566qnsPYMGDcrWCdQ9119/fRx00EHZ627dusW6664b3377bfaP4O+++y7WXnvteO6557L/wa7MPQHK03LLLRdTpkyJ9dZbL1ZZZZVs2siRI+O9996LJk2axD333BPbbbfdbO9xP4D6Yb/99otbbrklu7Y7d+4cH3zwQZXLuSdA+Qbu6f8Ldtpppyw8n9Mf/vCH+MlPfjLbNPeDOaTAnfrhlFNOSQlbYfPNNy989913FdMvvvjibHrv3r1rtX1A/s4///zCaaedVnjggQcK48ePLzRr1iy73qszePDgbH67du0K7733XsX0IUOGFJo2bVpo06ZNYdKkSSVqPZCnm266qXDwwQcX3n777dmm/+9//yv06NEju/b32GOP2ea5J0D5evHFFwvTpk2ba/pVV12VXfcrrrhi4YcffqiY7n4A9cOTTz6ZXevp3wzpuXPnzlUu554A5Svlg+n6HjNmzAIt734wN4F7PTF9+vTCMsssk10Ar7322lzz11tvvWze8OHDa6V9QGnML3D/1a9+lc2/9NJL55p31FFHZfMuuuiiGm4lUGrpH8Pp+k73iPRvhiL3BKifUsCWru833nijYpr7AZS/qVOnZtf/Ouusk4Vm8wrc3ROgfC1s4O5+MDc13OuJl156Kb755pvs52A9evSYa/7OO++cPT/44IO10DpgSRnj4emnn57tnlCZ+wSUr1THPZk+fXpWtzVxT4D6K5WUSZo2bZo9ux9A/XDWWWfFhx9+GNdcc03FfaAq7glAkftB1RpXM50y88Ybb2TPG264YZXzi9PffPPNkrYLWHKMGjUqC9vSmA5pgNU5uU9A+Ur/c52k/7lOAxwl7glQP916663Z9Z8GU02PxP0Ayl+6fi+++OLYf//9K8Z7q457AtQPN9xwQ9YZp2HDhtG1a9esDvtqq6022zLuB1UTuNcT48aNy56rOvkrT//oo49K2i6g7twnWrZsGW3atIlJkyZlAyy2bt26xC0Eakoa4Cjp27dvNGvWLHvtngD1w4UXXpgNlpoGUH3nnXey1yuvvHLccccd0ahRo2wZ9wMob7NmzYoDDzwwu44vuOCC+S7vngD1w7nnnjvbn4877rg47bTTskeR+0HVlJSpJyZPnpw9L7XUUtVeAEk6+YH6aX73icS9AsrPI488kvVeSb3bzznnnIrp7glQPzz++ONx8803x6BBg7KwvWPHjlnYvtFGG1Us434A5e2KK66IYcOGZV/AtWvXbr7LuydAedtyyy2zX7yNHj06pk6dmvViP++886Jx48Zx+umnV3TWSdwPqiZwBwCop959993Ye++900jK2f9kF2u5A/XHk08+md0DUs+z559/Pisj07t37+x/rIHyl3qnnnrqqdl1v99++9V2c4AlwNlnn539P8Iaa6wRLVq0yMrJnHzyyXHfffdl888888ysdjvVE7jXE61atcqe0zdTVUk/IU3qy087gIW/TyTuFVA+Pv3006yETArZfv/738fRRx8923z3BKhf0s+9U93m9KuX1Ls9/Vw89XhN3A+gfB1xxBExY8aMbKDUBeWeAPXTNttsEz/96U/j66+/jqFDh2bT3A+qpoZ7PVEc1OCTTz6pcn5xevoJKVA/ze8+kf6STH+xLrvssvXqL0ooRxMnTsz+wZzGbkmDo1100UVzLeOeAPVTKi+12267xYgRI+LBBx+MjTfe2P0AythDDz2UfeF26KGHzjb9+++/r/iCvk+fPtnrO++8M9q3b++eAPVY+iXc8OHDY/z48dmf3Q+qJnCvJ4o/EX/ttdeqnF+cvt5665W0XcCSY6211soGS5wwYUL2D+tVVllltvnuE1AeUp3FX/3qV/H222/HwIED47rrrosGDRrMtZx7AtRfyy23XPacrv/E/QDKWwrDnnvuuSrnpeC9OK8YwrsnQP2Vfh1buS67+0HVlJSpJ3r27BnLLLNMNuDB66+/Ptf8NEhSsv3229dC64AlQarNttVWW2Wv77777rnmu09A3Td9+vTYYYcd4tVXX41tt902GxixUaNGVS7rngD1VzFc69y5c/bsfgDlK43hUNVjzJgxFfeB4rROnTpl09wToH5KofoLL7yQvd5www2zZ/eDahSoN0455ZRCOuRbbLFFYfLkyRXTL7744mx67969a7V9QM1r1qxZdr1XZ/Dgwdn8du3aFd57772K6UOGDMne26ZNm8KkSZNK1FogTz/++GNhwIAB2TXeq1evwpQpU+b7HvcEKE8vvvhi4dFHHy3MnDlztukzZswoXH755YWGDRsWWrRoURg3blzFPPcDqF/GjBmTXfOdO3eucr57ApSnl156qXDvvfdm/+8w5z2hZ8+e2XXfv3//2ea5H8ytQfpPdWE85SX9/CvVXksDG6y00krZoEipdmv68/LLLx+vvPJKNgIxUD4efvjhOOeccyr+nHq1ptv+pptuWjEtDYrWr1+/ij8fc8wxcdlll8VSSy0VW2+9dTaI0uDBg7P3pW+nd9xxx5J/DmDxpes6Xd/JgAEDYumll65yuVTPvVhOInFPgPJz0003ZeM3pGs9DZDarl27+PLLL+Ott97KarI2b948br755th1111ne5/7AdQfY8eOjdVXXz3r4f7BBx9UuYx7ApTvvxHSeA2pF3sa4yFlh2lsl5Qrdu/ePZ5++ulYYYUVZnuf+8HsBO71zLRp0+LPf/5z3H777fHxxx9H27Zto2/fvlkg16FDh9puHlBDf1nOy4033hj77bffXO+78sor45133ommTZvGZpttlgXzW2yxRQ23GKgpZ555Zpx11lnzXS79hLz4k/Ei9wQoL+k6v/7667PSMR9++GEWtqdrO1376WfhRx11VKy55ppVvtf9AOqHBQncE/cEKC/pWr7iiiuyzrkpN0w121O99m7dusUuu+wShx12WFZGpiruB/+PwB0AAAAAAHJg0FQAAAAAAMiBwB0AAAAAAHIgcAcAAAAAgBwI3AEAAAAAIAcCdwAAAAAAyIHAHQAAAAAAciBwBwAAAACAHAjcAQAAAAAgBwJ3AAAAAADIgcAdAAAAAAByIHAHAIAycuWVV8bZZ58dX3/9dW03BQAA6h2BOwAAlIkbb7wxfve738WPP/4Ybdq0qe3mAABAvSNwBwCAWtKgQYPskcLx6nqkn3/++dkyZ5555jzXNXr06DjqqKNiwIABcdZZZ9VQiwEAgHkRuAMAQC375ptv4pJLLlnk98+cOTP23nvvWH311eOWW27JAnoAAKD0BO4AAFCLUjjevHnzuOyyy2LSpEmLtI733nsvtt1223jggQeiVatWubcRAABYMAJ3AACoRQ0bNoyDDz44vv3227jooosWaR3dunXLSs506tQp9/YBAAALTuAOAAC17KSTTooWLVrEFVdcEV999dUCvadPnz5Z7/ixY8fONS9NS/PSMpWlUD5Nv+mmm2LEiBHxq1/9Kqsf37Zt29h1113jk08+yZabMmVKnHDCCVmAn3rfr7vuujFo0KBq2/LOO+/EfvvtF6uuumo0a9YsVlxxxdh9991j5MiRcy2btl2sSZ965qfl0vLpi4f77ruvYrlHHnkktt5661h22WWzNqy11lrZfqqu1j0AACwJBO4AAFDLVlpppTj00EPju+++iwsvvLDGtzd06NDo2bNnTJgwIStF065du7j77rvjF7/4RVZP/uc//3ncfPPNsfHGG8fmm28eb7/9dhbIP/7443OtK4XkPXr0yJZfbrnlon///lkt+X/961+xySabxPPPP19lG0aNGpWt/9VXX822l8L1Jk2aZPP+/Oc/R79+/eLZZ5+NjTbaKHbccceYOnVq/OUvf4lNN900Pv/88xrfRwAAsCgE7gAAsAQ48cQTY6mlloorr7wyC8Jr0jXXXBN//etfY/jw4XHXXXdlgfovf/nLrMf5FltsEa1bt44PP/wwC+GfeeaZuO6666JQKMSf/vSnuXrSp8FaU1A+ePDg+M9//pO955VXXsl6qP/www/Z/BkzZszVhjvvvDP22WefeP/997PXKcxPIfuwYcPi1FNPzWrRv/jii/Hkk09m8z/44IPYZZddsjYeccQRNbp/AABgUQncAQBgCZDKqhx22GFZOZfUk7sm/exnP8t61BelwPx3v/td9vrdd9+Nq6++Olq2bFkxP5WLSb3XX3755SxEL0qhfWpv6pGeAvvK+vbtm32ejz/+OB5++OG52rD88stnn7NRo0azTU9fOMyaNStrT+rNXpRK1aR5qfTOvffem60XAACWNAJ3AABYgnq5p6A7Bd41WTZlm222mWvaGmuskT2nuu1du3adbV4KxTt27JiF7V9++WXF9CeeeCJ7HjhwYJXb6dWrV/acysbMKQX0qUf/nF544YXsea+99ppr3gorrJC1PQXyL7300nw/JwAAlJrAHQAAlhCp13cql5LqlZ9//vk1tp1VVlllrmmphEt18yrPnz59esW04oCt6T1pINQ5H6kETFI5pC9abbXVqtzO//73v4rgvyrF6Z9++ul8PiUAAJRe41rYJgAAUI3jjz8+/va3v2V11k844YRFWkfqAT4vDRs2XKR51W1n3333nedylUvDFDVv3jwWRQryAQBgSSVwBwCAJUiqlZ7ql6e66Omx8sorV7lc06ZNs+fJkyfPNa9U9c07dOgQo0ePjosvvjjatWuXyzrT5x0zZkx89NFHsc4668w1v3KvegAAWNIoKQMAAEuYP/zhD9G6dev4+9//Xm3plJVWWil7fu+99+aaN3jw4CiFrbfeOntOg5jmpVj3/Y477phr3oQJE+Lxxx/Pern37Nkzt20CAEBeBO4AALCESb3FjzrqqKxe+g033FDlMr17986eU+/yVPO96Omnn46//vWvJftioEWLFnHcccfFPffcM9f81P5BgwbFJ598ssDrTDXsU1mbyy+/PIYPH14xfcaMGVnP/2nTpmWDtK666qq5fQ4AAMiLwB0AAJZAKcxeeumls4C5KnvssUestdZaMWTIkOjWrVvsvPPOsdlmm2W9zg877LCStHHNNdfMeqL/8MMPsdNOO0WXLl2if//+Wdu23HLL7IuDNHBqVYOmVmeTTTaJc845J7799tvYfPPNs8+T1pe2ddddd2XbuOqqq2r0cwEAwKISuAMAwBJo2WWXjWOOOaba+aln+VNPPZWF0d9991088sgjMXPmzCyUTr3ES2WHHXaIN998Mw4//PCs1EsqZ/Pwww/HF198Edtvv33861//qrIW+7ycfPLJ8dBDD2W9+IcNG5b1nm/WrFk2iOzQoUNjxRVXrLHPAwAAi6NBoVAoLNYaAAAAAAAAPdwBAAAAACAPAncAAAAAAMiBwB0AAAAAAHIgcAcAAAAAgBwI3AEAAAAAIAcCdwAAAAAAyIHAHQAAAAAAciBwBwAAAACAHAjcAQAAAAAgBwJ3AAAAAADIgcAdAAAAAAByIHAHAAAAAIAcCNwBAAAAACAHAncAAAAAAMiBwB0AAAAAAHIgcAcAAAAAgBwI3AEAAAAAIAcCdwAAAAAAyIHAHQAAAAAAciBwBwAAAACAHAjcAQAAAAAgBwJ3AAAAAADIgcAdAAAAAAByIHAHAAAAAIAcCNwBAAAAACAHAncAAAAAAMiBwB0AAAAAAGLx/X8K4Q9hnzIlXgAAAABJRU5ErkJggg==" alt="Frecuencias"/>
19
+ <pre>01: ██████���██████████················· 4
20
+ 02: █████████████████████············· 5
21
+ 03: ████████·························· 2
22
+ 04: ████████·························· 2
23
+ 05: ████······························ 1
24
+ 06: █████████████████████████········· 6
25
+ 07: █████████████████████············· 5
26
+ 08: ████████·························· 2
27
+ 09: █████████████████████████········· 6
28
+ 10: ████████·························· 2
29
+ 11: ████████████······················ 3
30
+ 12: ████████████······················ 3
31
+ 13: ████████████······················ 3
32
+ 14: ██████████████████████████████████ 8
33
+ 15: ████████████······················ 3
34
+ 16: ████████████······················ 3
35
+ 17: ████████████······················ 3
36
+ 18: ████······························ 1
37
+ 19: █████████████████················· 4
38
+ 20: ████████████······················ 3
39
+ 21: █████████████████················· 4
40
+ 22: █████████████████················· 4
41
+ 23: █████████████████████████········· 6
42
+ 24: ██████████████████████████████████ 8
43
+ 25: ████████·························· 2
44
+ 26: ████······························ 1
45
+ 27: ████████·························· 2
46
+ 28: █████████████████████████········· 6
47
+ 29: █████████████████████············· 5
48
+ 30: ████████████······················ 3
49
+ 31: █████████████████················· 4
50
+ 32: █████████████████████████········· 6
51
+ 33: █████████████████████············· 5
52
+ 34: ████████████······················ 3
53
+ 35: █████████████████················· 4
54
+ 36: █████████████████████████········· 6
55
+ 37: █████████████████████············· 5
56
+ 38: █████████████████████████········· 6
57
+ 39: ████████████······················ 3
58
+ 40: ████······························ 1
59
+ 41: ████████·························· 2
60
+ 42: ████████·························· 2
61
+ 43: █████████████████████············· 5
62
+ 44: ████████████······················ 3
63
+ 45: █████████████████················· 4
64
+ 46: █████████████████████············· 5
65
+ 47: ████████·························· 2
66
+ 48: ████████·························· 2
67
+ 49: ████████·························· 2</pre>
68
+
69
+ <h2>Chi-cuadrado</h2>
70
+ <p>Chi2 = 37.89349112426035 &nbsp;&nbsp; p-value = 0.7296561046478667</p>
71
+ <p class="small">Si p-value es muy pequeño (&lt; 0.05), indica desviación respecto a uniformidad; recuerda que los sorteos son procesos aleatorios.</p>
72
+
73
+ <h2>Disclaimer</h2>
74
+ <p class="small">Este sistema es informativo y educativo. La lotería es aleatoria. Juega con responsabilidad.</p>
75
+ </body>
76
+ </html>
proyecto_tinka_ml/proyecto_tinka_ml/db_connector.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Conexión y consultas a MySQL (XAMPP) con caché local robusto."""
2
+ from __future__ import annotations
3
+ from pathlib import Path
4
+ from urllib.parse import quote_plus
5
+ import pandas as pd
6
+ from sqlalchemy import create_engine
7
+
8
+ from config import DB_CONFIG, TABLE_NAME, CACHE_FILE
9
+ from utils import load_json, save_json
10
+
11
+ SQL_BASE = f"""SELECT id_sorteo, fecha_sorteo, numeros, boliyapa, jackpot,
12
+ COALESCE(created_at, NOW()) AS created_at
13
+ FROM {TABLE_NAME}
14
+ WHERE numeros IS NOT NULL AND numeros <> ''
15
+ ORDER BY fecha_sorteo ASC, id_sorteo ASC
16
+ """
17
+
18
+ def get_engine():
19
+ user = DB_CONFIG.get("user", "")
20
+ pwd = quote_plus(DB_CONFIG.get("password", ""))
21
+ host = DB_CONFIG.get("host", "localhost")
22
+ port = DB_CONFIG.get("port", 3306)
23
+ db = DB_CONFIG.get("database", "")
24
+ url = f"mysql+mysqlconnector://{user}:{pwd}@{host}:{port}/{db}"
25
+ return create_engine(url, pool_pre_ping=True, pool_recycle=1800)
26
+
27
+ def fetch_from_db() -> pd.DataFrame:
28
+ eng = get_engine()
29
+ try:
30
+ df = pd.read_sql_query(SQL_BASE, eng)
31
+ return df
32
+ finally:
33
+ eng.dispose()
34
+
35
+ def load_cached():
36
+ data = load_json(CACHE_FILE, default=None)
37
+ if not data: return None
38
+ try:
39
+ df = pd.DataFrame(data)
40
+ if "fecha_sorteo" in df.columns:
41
+ df["fecha_sorteo"] = pd.to_datetime(df["fecha_sorteo"]).dt.date
42
+ return df
43
+ except Exception:
44
+ return None
45
+
46
+ def save_cache(df: pd.DataFrame) -> None:
47
+ df = df.copy()
48
+ for col in ("fecha_sorteo", "created_at"):
49
+ if col in df.columns:
50
+ df[col] = df[col].astype(str)
51
+ save_json(CACHE_FILE, df.to_dict(orient="records"))
52
+
53
+ def get_data(use_cache: bool = True):
54
+ if use_cache:
55
+ cached = load_cached()
56
+ if cached is not None and len(cached) > 0:
57
+ return cached.copy()
58
+ df = fetch_from_db()
59
+ if "fecha_sorteo" in df.columns:
60
+ df["fecha_sorteo"] = pd.to_datetime(df["fecha_sorteo"]).dt.date
61
+ save_cache(df)
62
+ return df
63
+
64
+ def refresh_cache():
65
+ df = fetch_from_db()
66
+ if "fecha_sorteo" in df.columns:
67
+ df["fecha_sorteo"] = pd.to_datetime(df["fecha_sorteo"]).dt.date
68
+ save_cache(df)
69
+ return df
proyecto_tinka_ml/proyecto_tinka_ml/generador.py ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Generación y evaluación de combinaciones a partir de las estadísticas."""
2
+ from __future__ import annotations
3
+ from typing import List, Dict, Tuple
4
+ import random
5
+ import numpy as np
6
+
7
+ from config import TOTAL_NUMBERS, COMBINATION_SIZE, SUM_RANGE
8
+
9
+ def _validate_combo(combo: List[int]) -> bool:
10
+ if len(combo) != COMBINATION_SIZE: return False
11
+ if len(set(combo)) != COMBINATION_SIZE: return False
12
+ if not all(1 <= x <= TOTAL_NUMBERS for x in combo): return False
13
+ return True
14
+
15
+ def _meets_heuristics(combo: List[int]) -> bool:
16
+ s = sum(combo)
17
+ if not (SUM_RANGE[0] <= s <= SUM_RANGE[1]):
18
+ return False
19
+ ev = sum(1 for x in combo if x % 2 == 0)
20
+ od = COMBINATION_SIZE - ev
21
+ if abs(ev - od) > 2:
22
+ return False
23
+ return True
24
+
25
+ def _weighted_choice(population: List[int], weights: List[float], k: int) -> List[int]:
26
+ arr = np.array(population)
27
+ w = np.array(weights, dtype=float)
28
+ if w.sum() <= 0:
29
+ w = np.ones_like(w) / len(w)
30
+ else:
31
+ w = w / w.sum()
32
+ picks = set()
33
+ for _ in range(2000):
34
+ x = np.random.choice(arr, p=w)
35
+ picks.add(int(x))
36
+ if len(picks) == k:
37
+ break
38
+ if len(picks) < k:
39
+ rest = [p for p in population if p not in picks]
40
+ random.shuffle(rest)
41
+ picks.update(rest[:(k-len(picks))])
42
+ return sorted(picks)
43
+
44
+ def _rango_bucket(n: int) -> int:
45
+ if 1 <= n <= 9: return 0
46
+ elif 10 <= n <= 18: return 1
47
+ elif 19 <= n <= 27: return 2
48
+ elif 28 <= n <= 36: return 3
49
+ elif 37 <= n <= 45: return 4
50
+ return 4
51
+
52
+ def _consecutivos(combo: List[int]) -> int:
53
+ arr = sorted(combo)
54
+ return sum(1 for a, b in zip(arr, arr[1:]) if b == a + 1)
55
+
56
+ # Estrategias clásicas
57
+ def estrategia_frecuencia_pura(freq_abs: Dict[int,int], n_combos: int = 10) -> List[List[int]]:
58
+ top = sorted(freq_abs.items(), key=lambda x: (-x[1], x[0]))
59
+ top_pool = [k for k,_ in top[:min(25, len(top))]]
60
+ combos, tries = [], 0
61
+ while len(combos) < n_combos and tries < 8000:
62
+ c = sorted(random.sample(top_pool, COMBINATION_SIZE))
63
+ if _validate_combo(c) and _meets_heuristics(c) and c not in combos:
64
+ combos.append(c)
65
+ tries += 1
66
+ return combos
67
+
68
+ def estrategia_equilibrio_hot_cold(freq_abs: Dict[int,int], hot_15: List[int], cold_15: List[int], n_combos: int = 10) -> List[List[int]]:
69
+ combos, tries = [], 0
70
+ pool_cold = cold_15[:]
71
+ pool_hot = hot_15[:]
72
+ if len(pool_hot) < 3 or len(pool_cold) < 3:
73
+ return estrategia_frecuencia_pura(freq_abs, n_combos)
74
+ while len(combos) < n_combos and tries < 8000:
75
+ c = sorted(random.sample(pool_hot, 3) + random.sample(pool_cold, 3))
76
+ if _validate_combo(c) and _meets_heuristics(c) and c not in combos:
77
+ combos.append(c)
78
+ tries += 1
79
+ return combos
80
+
81
+ def estrategia_temporal_inteligente(freq_last: Dict[int,int], hot_cycle: List[int], n_combos: int = 10) -> List[List[int]]:
82
+ keys = list(range(1, TOTAL_NUMBERS+1))
83
+ weights = [freq_last.get(k, 0) + (1.5 if k in hot_cycle else 0.0) + 0.01 for k in keys]
84
+ combos, tries = [], 0
85
+ while len(combos) < n_combos and tries < 10000:
86
+ c = _weighted_choice(keys, weights, COMBINATION_SIZE)
87
+ c = sorted(c)
88
+ if _validate_combo(c) and _meets_heuristics(c) and c not in combos:
89
+ combos.append(c)
90
+ tries += 1
91
+ return combos
92
+
93
+ def estrategia_patrones_detectados(pairs_top: List[Dict], trios_top: List[Dict], n_combos: int = 10) -> List[List[int]]:
94
+ combos, tries = [], 0
95
+ pairs = [tuple(p['pair']) for p in pairs_top]
96
+ trios = [tuple(t['trio']) for t in trios_top]
97
+ while len(combos) < n_combos and tries < 12000:
98
+ base = set()
99
+ if trios and random.random() < 0.6:
100
+ base.update(random.choice(trios))
101
+ guard = 0
102
+ while len(base) < 4 and pairs and guard < 10:
103
+ cand = random.choice(pairs)
104
+ if not (set(cand) & base):
105
+ base.update(cand)
106
+ guard += 1
107
+ rest = [x for x in range(1, TOTAL_NUMBERS+1) if x not in base]
108
+ random.shuffle(rest)
109
+ while len(base) < 6 and rest:
110
+ base.add(rest.pop())
111
+ c = sorted(list(base))[:6]
112
+ if _validate_combo(c) and _meets_heuristics(c) and c not in combos:
113
+ combos.append(c)
114
+ tries += 1
115
+ return combos
116
+
117
+ def estrategia_random_ponderado(freq_abs: Dict[int,int], n_combos: int = 10) -> List[List[int]]:
118
+ keys = list(range(1, TOTAL_NUMBERS+1))
119
+ raw = [freq_abs.get(k, 0) + 0.01 for k in keys]
120
+ combos, tries = [], 0
121
+ while len(combos) < n_combos and tries < 8000:
122
+ c = _weighted_choice(keys, raw, COMBINATION_SIZE)
123
+ c = sorted(c)
124
+ if _validate_combo(c) and _meets_heuristics(c) and c not in combos:
125
+ combos.append(c)
126
+ tries += 1
127
+ return combos
128
+
129
+ # Scoring ML
130
+ def score_combo_ml(combo: List[int], probs: Dict[int, float]) -> float:
131
+ eps = 1e-9
132
+ ll = sum(np.log(max(probs.get(x, eps), eps)) for x in combo) * 10.0
133
+ suma = sum(combo)
134
+ penalty_sum = abs(135 - suma) * 1.0
135
+ ev = sum(1 for x in combo if x % 2 == 0)
136
+ od = COMBINATION_SIZE - ev
137
+ penalty_parity = abs(ev - od) * 2.0
138
+ consec = _consecutivos(combo)
139
+ penalty_consec = max(0, consec - 1) * 3.0
140
+ buckets = [0,0,0,0,0]
141
+ for x in combo:
142
+ if 1 <= x <= 9: buckets[0]+=1
143
+ elif 10 <= x <= 18: buckets[1]+=1
144
+ elif 19 <= x <= 27: buckets[2]+=1
145
+ elif 28 <= x <= 36: buckets[3]+=1
146
+ elif 37 <= x <= 45: buckets[4]+=1
147
+ cobertura = sum(1 for b in buckets if b > 0)
148
+ penalty_bucket = 0.0
149
+ if max(buckets) > 3:
150
+ penalty_bucket += (max(buckets) - 3) * 2.0
151
+ if cobertura < 3:
152
+ penalty_bucket += (3 - cobertura) * 2.0
153
+ return float(ll - penalty_sum - penalty_parity - penalty_consec - penalty_bucket)
154
+
155
+ def rankear_combos_ml(combos: List[List[int]], probs: Dict[int, float]):
156
+ scored = []
157
+ seen = set()
158
+ for c in combos:
159
+ t = tuple(sorted(c))
160
+ if t in seen:
161
+ continue
162
+ seen.add(t)
163
+ scored.append((list(t), score_combo_ml(list(t), probs)))
164
+ scored.sort(key=lambda x: x[1], reverse=True)
165
+ return scored
proyecto_tinka_ml/proyecto_tinka_ml/main.py ADDED
@@ -0,0 +1,250 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Interfaz de consola (menú) para el sistema Tinka con opción ML."""
2
+ from __future__ import annotations
3
+ from pathlib import Path
4
+ import pandas as pd
5
+
6
+ from config import DATA_DIR, COMBOS_FILE
7
+ from db_connector import get_data, refresh_cache
8
+ from analizador import analisis_completo, analisis_frecuencias, analisis_temporal
9
+ from generador import (
10
+ estrategia_frecuencia_pura,
11
+ estrategia_equilibrio_hot_cold,
12
+ estrategia_temporal_inteligente,
13
+ estrategia_patrones_detectados,
14
+ estrategia_random_ponderado,
15
+ rankear_combos_ml,
16
+ )
17
+ from utils import parse_numbers, save_json, load_json
18
+ from visualizador import render_ascii_hist, html_report
19
+ from ml import (
20
+ beta_binomial_posteriors,
21
+ beta_binomial_posteriors_ewma,
22
+ blend_probabilities,
23
+ save_probabilities,
24
+ thompson_sampling_pool,
25
+ )
26
+
27
+ def pause():
28
+ input("\nPresiona ENTER para continuar...")
29
+
30
+ def _int_input_default(prompt: str, default: int) -> int:
31
+ s = input(prompt).strip()
32
+ if not s:
33
+ return default
34
+ try:
35
+ v = int(s)
36
+ if v <= 0: return default
37
+ return v
38
+ except Exception:
39
+ return default
40
+
41
+ def show_dashboard(df: pd.DataFrame):
42
+ stats = analisis_completo(df)
43
+ print("\n=== DASHBOARD RÁPIDO ===\n")
44
+ frec = stats['frecuencias']
45
+ print("Top 15 calientes:", frec['hot_15'])
46
+ print("Top 15 fríos :", frec['cold_15'])
47
+ print("En racha (ult10):", frec['en_racha_ult10'])
48
+ print("Dormidos (>20) :", frec['dormidos_mas20'])
49
+ print("\nHistograma (ASCII):\n")
50
+ print(render_ascii_hist(frec['freq_abs']))
51
+ print("\nChi-cuadrado:", stats['chi_cuadrado'])
52
+ out_html = DATA_DIR / "reporte.html"
53
+ out_html.write_text(html_report(stats), encoding="utf-8")
54
+ print(f"\nReporte HTML exportado en: {out_html}")
55
+
56
+ def analisis_por_numero(df: pd.DataFrame):
57
+ try:
58
+ n = int(input("Número (1-45): ").strip())
59
+ except Exception:
60
+ print("Entrada inválida"); return
61
+ if not (1 <= n <= 45):
62
+ print("Fuera de rango"); return
63
+ frec = analisis_frecuencias(df)
64
+ fa = frec['freq_abs'].get(n, 0)
65
+ fr = frec['freq_rel'].get(n, 0.0)
66
+ ult10 = n in frec['en_racha_ult10']
67
+ dorm = n in frec['dormidos_mas20']
68
+ print(f"\nNúmero {n}: freq_abs={fa}, freq_rel={fr:.4f}, en_racha_ult10={ult10}, dormido>20={dorm}\n")
69
+
70
+ def _imprimir_combos(combos):
71
+ for i, c in enumerate(combos, 1):
72
+ print(f"{i:02d})", " ".join(f"{x:02d}" for x in c))
73
+
74
+ def generar_combinaciones(df: pd.DataFrame):
75
+ stats = analisis_completo(df)
76
+ frec = stats['frecuencias']
77
+ cooc = stats['coocurrencias']
78
+ temp = analisis_temporal(df)
79
+ last50 = temp['ventanas']['50']
80
+
81
+ print("\nEstrategias:")
82
+ print(" 1) Frecuencia pura")
83
+ print(" 2) Equilibrio caliente-frío")
84
+ print(" 3) Temporal inteligente (últimos 50)")
85
+ print(" 4) Patrones detectados (pares/tríos)")
86
+ print(" 5) Random ponderado")
87
+ op = input("Elige 1-5: ").strip()
88
+
89
+ n = _int_input_default("¿Cuántas combinaciones quieres generar? [10 por defecto]: ", 10)
90
+
91
+ combos = []
92
+ if op == "1":
93
+ combos = estrategia_frecuencia_pura(frec['freq_abs'], n_combos=n)
94
+ etiqueta = "frecuencia_pura"
95
+ elif op == "2":
96
+ combos = estrategia_equilibrio_hot_cold(frec['freq_abs'], frec['hot_15'], frec['cold_15'], n_combos=n)
97
+ etiqueta = "equilibrio_hot_cold"
98
+ elif op == "3":
99
+ combos = estrategia_temporal_inteligente(last50['freq_abs'], last50['hot_15'], n_combos=n)
100
+ etiqueta = "temporal_inteligente_50"
101
+ elif op == "4":
102
+ combos = estrategia_patrones_detectados(cooc['pairs_top20'], cooc['trios_top20'], n_combos=n)
103
+ etiqueta = "patrones_detectados"
104
+ elif op == "5":
105
+ combos = estrategia_random_ponderado(frec['freq_abs'], n_combos=n)
106
+ etiqueta = "random_ponderado"
107
+ else:
108
+ print("Opción no válida"); return
109
+
110
+ if not combos:
111
+ print("\nNo se pudieron generar combinaciones."); return
112
+
113
+ print("\n=== Combinaciones sugeridas ===\n")
114
+ _imprimir_combos(combos)
115
+
116
+ record = load_json(COMBOS_FILE, default=[])
117
+ record.append({"estrategia": etiqueta, "n": n, "combos": combos})
118
+ save_json(COMBOS_FILE, record)
119
+ print(f"\nGuardado en {COMBOS_FILE}")
120
+
121
+ def comparar_mi_combinacion(df: pd.DataFrame):
122
+ s = input("Ingresa tus 6 números separados por espacio: ").strip()
123
+ arr = sorted(parse_numbers(s))
124
+ if len(arr) != 6 or min(arr) < 1 or max(arr) > 45 or len(set(arr)) != 6:
125
+ print("Entrada inválida"); return
126
+ stats = analisis_completo(df)
127
+ frec = stats['frecuencias']
128
+ hot = set(frec['hot_15'])
129
+ cold = set(frec['cold_15'])
130
+ score = sum(frec['freq_abs'].get(x,0) for x in arr)
131
+ suma = sum(arr)
132
+ ev = sum(1 for x in arr if x % 2 == 0)
133
+ od = 6 - ev
134
+ in_hot = [x for x in arr if x in hot]
135
+ in_cold = [x for x in arr if x in cold]
136
+ print("\n=== Evaluación ===")
137
+ print("Tu combinación :", " ".join(f"{x:02d}" for x in arr))
138
+ print("Suma :", suma, "(recomendado 90-180)")
139
+ print("Pares/Impares :", ev, "/", od)
140
+ print("En HOT :", in_hot)
141
+ print("En COLD :", in_cold)
142
+ print("Puntuación (sum freq_abs):", score)
143
+
144
+ def ver_mejores_historicas(df: pd.DataFrame):
145
+ frec = analisis_frecuencias(df)['freq_abs']
146
+ rows = []
147
+ for _, r in df.iterrows():
148
+ arr = sorted(parse_numbers(r['numeros']))
149
+ s = sum(arr)
150
+ score = sum(frec.get(x,0) for x in arr)
151
+ penal = abs(135 - s)
152
+ rows.append({"id_sorteo": int(r['id_sorteo']), "fecha": str(r['fecha_sorteo']),
153
+ "combo": " ".join(f"{x:02d}" for x in arr),
154
+ "suma": s, "score": score - penal})
155
+ top = sorted(rows, key=lambda x: x['score'], reverse=True)[:10]
156
+ print("\n=== Top 10 históricas por score heurístico ===\n")
157
+ for i, row in enumerate(top, 1):
158
+ print(f"{i:02d}) {row['fecha']} id={row['id_sorteo']} {row['combo']} score={row['score']:.2f}")
159
+
160
+ def exportar_analisis(df: pd.DataFrame):
161
+ html = html_report(analisis_completo(df))
162
+ out = DATA_DIR / "reporte.html"
163
+ out.write_text(html, encoding="utf-8")
164
+ print(f"Reporte exportado en {out}")
165
+
166
+ def actualizar_cache():
167
+ df = refresh_cache()
168
+ print(f"Cache actualizado. Registros: {len(df)}")
169
+
170
+ # -- Opción 8: Recomendación automática (ML)
171
+ def recomendacion_ml_menu(df: pd.DataFrame):
172
+ print("\nCalculando probabilidades bayesianas (global + reciente)...")
173
+ posts_global = beta_binomial_posteriors(df, prior_strength=30.0, p0=(6.0/45.0))
174
+ posts_recent = beta_binomial_posteriors_ewma(df, halflife_draws=50, prior_strength=15.0, p0=(6.0/45.0))
175
+ probs_blend = blend_probabilities(posts_global, posts_recent, w_recent=0.30)
176
+ save_probabilities(posts_global, posts_recent, probs_blend)
177
+
178
+ n = _int_input_default("¿Cuántas recomendaciones quieres? [1 por defecto]: ", 1)
179
+
180
+ stats = analisis_completo(df)
181
+ frec = stats['frecuencias']
182
+ cooc = stats['coocurrencias']
183
+ temp = analisis_temporal(df)
184
+ last50 = temp['ventanas']['50']
185
+
186
+ pool = []
187
+ N = max(10, n * 12)
188
+ pool += estrategia_equilibrio_hot_cold(frec['freq_abs'], frec['hot_15'], frec['cold_15'], n_combos=N)
189
+ pool += estrategia_temporal_inteligente(last50['freq_abs'], last50['hot_15'], n_combos=N)
190
+ pool += estrategia_random_ponderado(frec['freq_abs'], n_combos=N // 2)
191
+ pool += estrategia_patrones_detectados(cooc['pairs_top20'], cooc['trios_top20'], n_combos=max(5, n*6))
192
+
193
+ # Thompson Sampling extra (con posts recientes para mayor reactividad)
194
+ from ml import thompson_sampling_pool
195
+ pool += thompson_sampling_pool(posts_recent, n_combos=max(10, n*10), k=6)
196
+
197
+ if not pool:
198
+ print("No se pudieron generar candidatas. Actualiza la base y reintenta."); return
199
+
200
+ ranked = rankear_combos_ml(pool, probs_blend)
201
+ topn = ranked[:n]
202
+
203
+ print("\n=== Recomendación automática (ML) ===\n")
204
+ for i, (combo, score) in enumerate(topn, 1):
205
+ print(f"{i:02d})", " ".join(f"{x:02d}" for x in combo), f" score={score:.2f}")
206
+
207
+ record = load_json(COMBOS_FILE, default=[])
208
+ record.append({"estrategia": "auto_ml", "n": n,
209
+ "ranked": [{"combo": c, "score": float(s)} for (c, s) in topn]})
210
+ save_json(COMBOS_FILE, record)
211
+ print(f"\nGuardado en {COMBOS_FILE}")
212
+
213
+ def main():
214
+ print("""====================================================
215
+ SISTEMA DE ANÁLISIS Y PREDICCIÓN — TINKA
216
+ (Educativo/Estadístico) — by tu asesor
217
+ ====================================================
218
+ AVISO: La lotería es aleatoria. Este sistema NO garantiza aciertos.
219
+ Usa la información con responsabilidad.
220
+ """ )
221
+ df = get_data(use_cache=True)
222
+ if len(df)==0:
223
+ print("No hay datos en la base. Ejecuta tu scraper primero.")
224
+ return
225
+ while True:
226
+ print("""Menú:
227
+ 1) Ver dashboard completo de estadísticas
228
+ 2) Análisis específico por número
229
+ 3) Generar combinaciones (elegir estrategia y cantidad)
230
+ 4) Comparar mi combinación vs estadísticas
231
+ 5) Ver mejores combinaciones históricas
232
+ 6) Exportar análisis a HTML
233
+ 7) Actualizar datos desde BD (refresh cache)
234
+ 8) Recomendación automática (ML)
235
+ 0) Salir
236
+ """ )
237
+ op = input("Elige opción: ").strip()
238
+ if op == "1": show_dashboard(df); pause()
239
+ elif op == "2": analisis_por_numero(df); pause()
240
+ elif op == "3": generar_combinaciones(df); pause()
241
+ elif op == "4": comparar_mi_combinacion(df); pause()
242
+ elif op == "5": ver_mejores_historicas(df); pause()
243
+ elif op == "6": exportar_analisis(df); pause()
244
+ elif op == "7": df = refresh_cache(); print("Datos recargados."); pause()
245
+ elif op == "8": recomendacion_ml_menu(df); pause()
246
+ elif op == "0": print("¡Hasta luego!"); break
247
+ else: print("Opción inválida")
248
+
249
+ if __name__ == "__main__":
250
+ main()
proyecto_tinka_ml/proyecto_tinka_ml/ml.py ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ML ligero: Beta-Binomial, EWMA y Thompson Sampling."""
2
+ from __future__ import annotations
3
+ from typing import Dict, List, Tuple
4
+ from collections import defaultdict
5
+ import numpy as np
6
+ import pandas as pd
7
+ from config import DATA_DIR
8
+ from utils import parse_numbers, save_json, load_json
9
+
10
+ PROBS_FILE = DATA_DIR / "probabilidades.json"
11
+
12
+ def _counts_from_df(df: pd.DataFrame) -> Dict[int, int]:
13
+ cnt = defaultdict(int)
14
+ for _, r in df.iterrows():
15
+ for n in parse_numbers(r['numeros']):
16
+ cnt[int(n)] += 1
17
+ return dict(cnt)
18
+
19
+ def _counts_ewma(df: pd.DataFrame, halflife_draws: int = 50) -> Dict[int, float]:
20
+ if len(df) == 0:
21
+ return {i: 0.0 for i in range(1, 46)}
22
+ gamma = 0.5 ** (1.0 / max(1, halflife_draws))
23
+ w = 1.0
24
+ cnt = {i: 0.0 for i in range(1, 46)}
25
+ total_weight = 0.0
26
+ for _, r in df.iloc[::-1].iterrows():
27
+ nums = set(parse_numbers(r['numeros']))
28
+ for i in range(1, 46):
29
+ if i in nums:
30
+ cnt[i] += w
31
+ total_weight += w
32
+ w *= gamma
33
+ cnt['__total_weight__'] = total_weight
34
+ return cnt
35
+
36
+ def beta_binomial_posteriors(df: pd.DataFrame, prior_strength: float = 30.0, p0: float = 6.0/45.0):
37
+ s = _counts_from_df(df)
38
+ D = float(len(df))
39
+ a0 = prior_strength * p0
40
+ b0 = prior_strength * (1.0 - p0)
41
+ posts = {}
42
+ for i in range(1, 46):
43
+ successes = float(s.get(i, 0))
44
+ failures = max(0.0, D - successes)
45
+ alpha = a0 + successes
46
+ beta = b0 + failures
47
+ posts[i] = {"alpha": alpha, "beta": beta, "p": alpha/(alpha+beta)}
48
+ return posts
49
+
50
+ def beta_binomial_posteriors_ewma(df: pd.DataFrame, halflife_draws: int = 50, prior_strength: float = 15.0, p0: float = 6.0/45.0):
51
+ ew = _counts_ewma(df, halflife_draws=halflife_draws)
52
+ total_w = float(ew.get('__total_weight__', 0.0))
53
+ a0 = prior_strength * p0
54
+ b0 = prior_strength * (1.0 - p0)
55
+ posts = {}
56
+ for i in range(1, 46):
57
+ successes = float(ew.get(i, 0.0))
58
+ failures = max(0.0, total_w - successes)
59
+ alpha = a0 + successes
60
+ beta = b0 + failures
61
+ posts[i] = {"alpha": alpha, "beta": beta, "p": alpha/(alpha+beta)}
62
+ return posts
63
+
64
+ def blend_probabilities(global_posts, recent_posts, w_recent: float = 0.30):
65
+ w_recent = max(0.0, min(1.0, w_recent))
66
+ out = {}
67
+ for i in range(1, 46):
68
+ pg = float(global_posts[i]["p"])
69
+ pr = float(recent_posts[i]["p"])
70
+ out[i] = (1.0 - w_recent) * pg + w_recent * pr
71
+ return out
72
+
73
+ def save_probabilities(probs_global, probs_recent, probs_blend):
74
+ data = {"global": probs_global, "recent": probs_recent, "blend": probs_blend}
75
+ save_json(PROBS_FILE, data)
76
+
77
+ def load_probabilities():
78
+ return load_json(PROBS_FILE, default=None)
79
+
80
+ def thompson_sampling_combination(posts, k: int = 6):
81
+ draws = []
82
+ for i in range(1, 46):
83
+ a = float(posts[i]['alpha']); b = float(posts[i]['beta'])
84
+ a = max(a, 1e-3); b = max(b, 1e-3)
85
+ theta = np.random.beta(a, b)
86
+ draws.append((i, float(theta)))
87
+ draws.sort(key=lambda x: x[1], reverse=True)
88
+ return sorted([i for (i, t) in draws[:k]])
89
+
90
+ def thompson_sampling_pool(posts, n_combos: int = 10, k: int = 6):
91
+ combos = []
92
+ seen = set()
93
+ tries = 0
94
+ while len(combos) < n_combos and tries < n_combos * 200:
95
+ c = tuple(thompson_sampling_combination(posts, k=k))
96
+ if c not in seen:
97
+ combos.append(list(c))
98
+ seen.add(c)
99
+ tries += 1
100
+ return combos
proyecto_tinka_ml/proyecto_tinka_ml/requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ pandas
2
+ numpy
3
+ mysql-connector-python
4
+ matplotlib
5
+ scipy
6
+ SQLAlchemy
proyecto_tinka_ml/proyecto_tinka_ml/utils.py ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Funciones auxiliares y utilidades comunes."""
2
+ from __future__ import annotations
3
+ from typing import List
4
+ from pathlib import Path
5
+ from math import sqrt
6
+ import json, os
7
+
8
+ def ensure_dirs(path: Path) -> None:
9
+ Path(path).parent.mkdir(parents=True, exist_ok=True)
10
+
11
+ def parse_numbers(s: str) -> List[int]:
12
+ nums = []
13
+ for token in s.replace(',', ' ').split():
14
+ token = token.strip()
15
+ if token.isdigit():
16
+ nums.append(int(token))
17
+ return nums
18
+
19
+ def is_prime(n: int) -> bool:
20
+ if n < 2: return False
21
+ if n in (2,3): return True
22
+ if n % 2 == 0: return False
23
+ r = int(sqrt(n)); f = 3
24
+ while f <= r:
25
+ if n % f == 0: return False
26
+ f += 2
27
+ return True
28
+
29
+ def ascii_bar(label: str, value: int, max_value: int, width: int = 40) -> str:
30
+ if max_value <= 0:
31
+ bar = ""
32
+ else:
33
+ fill = int((value / max_value) * width)
34
+ bar = "█" * fill + "·" * (width - fill)
35
+ return f"{label:>2}: {bar} {value}"
36
+
37
+ def save_json(path: Path, data) -> None:
38
+ ensure_dirs(path)
39
+ tmp = str(path) + ".tmp"
40
+ with open(tmp, "w", encoding="utf-8") as f:
41
+ json.dump(data, f, ensure_ascii=False, indent=2, default=str)
42
+ os.replace(tmp, path)
43
+
44
+ def load_json(path: Path, default=None):
45
+ p = Path(path)
46
+ if not p.exists():
47
+ return default
48
+ try:
49
+ txt = p.read_text(encoding="utf-8").strip()
50
+ if not txt:
51
+ return default
52
+ return json.loads(txt)
53
+ except Exception:
54
+ return default
proyecto_tinka_ml/proyecto_tinka_ml/visualizador.py ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Visualizaciones básicas: ASCII y PNG/HTML."""
2
+ from __future__ import annotations
3
+ from typing import Dict
4
+ import base64
5
+ from io import BytesIO
6
+ import matplotlib.pyplot as plt
7
+ from utils import ascii_bar
8
+
9
+ def render_ascii_hist(freq_abs: Dict[int,int]) -> str:
10
+ maxv = max(freq_abs.values()) if freq_abs else 0
11
+ lines = []
12
+ for n, v in sorted(freq_abs.items()):
13
+ lines.append(ascii_bar(f"{n:02d}", v, maxv, width=34))
14
+ return "\n".join(lines)
15
+
16
+ def plot_freq_png(freq_abs: Dict[int,int]) -> bytes:
17
+ xs = list(sorted(freq_abs.keys()))
18
+ ys = [freq_abs[k] for k in xs]
19
+ fig, ax = plt.subplots(figsize=(10,4))
20
+ ax.bar(xs, ys)
21
+ ax.set_title("Frecuencia absoluta de números (1-45)")
22
+ ax.set_xlabel("Número")
23
+ ax.set_ylabel("Frecuencia")
24
+ buf = BytesIO()
25
+ fig.tight_layout()
26
+ fig.savefig(buf, format="png", dpi=150)
27
+ plt.close(fig)
28
+ return buf.getvalue()
29
+
30
+ def html_report(stats: Dict) -> str:
31
+ png = plot_freq_png(stats['frecuencias']['freq_abs'])
32
+ b64 = base64.b64encode(png).decode("ascii")
33
+ chi2 = stats['chi_cuadrado']
34
+ html = f"""<!DOCTYPE html>
35
+ <html lang="es">
36
+ <head>
37
+ <meta charset="utf-8"/>
38
+ <title>Reporte Tinka</title>
39
+ <style>
40
+ body {{ font-family: Arial, Helvetica, sans-serif; margin: 20px; }}
41
+ h1, h2 {{ margin: 0.4em 0; }}
42
+ pre {{ background: #f6f8fa; padding: 12px; overflow: auto; }}
43
+ .small {{ color:#555; font-size: 0.9em; }}
44
+ </style>
45
+ </head>
46
+ <body>
47
+ <h1>Reporte de Análisis — Tinka</h1>
48
+ <p class="small">Este reporte se genera a partir de los resultados históricos presentes en tu base de datos.</p>
49
+
50
+ <h2>Frecuencias</h2>
51
+ <img src="data:image/png;base64,{b64}" alt="Frecuencias"/>
52
+ <pre>{render_ascii_hist(stats['frecuencias']['freq_abs'])}</pre>
53
+
54
+ <h2>Chi-cuadrado</h2>
55
+ <p>Chi2 = {chi2.get('chi2')} &nbsp;&nbsp; p-value = {chi2.get('p_value')}</p>
56
+ <p class="small">Si p-value es muy pequeño (&lt; 0.05), indica desviación respecto a uniformidad; recuerda que los sorteos son procesos aleatorios.</p>
57
+
58
+ <h2>Disclaimer</h2>
59
+ <p class="small">Este sistema es informativo y educativo. La lotería es aleatoria. Juega con responsabilidad.</p>
60
+ </body>
61
+ </html>
62
+ """
63
+ return html