relvistcb commited on
Commit
6bd33ef
·
verified ·
1 Parent(s): c791fed

Upload 9 files

Browse files
Lucky For Life.csv CHANGED
@@ -1,4 +1,9 @@
1
  date,b1,b2,b3,b4,b5,lucky_ball
 
 
 
 
 
2
  1/24/2026,8,17,25,40,44,7
3
  1/23/2026,6,16,17,18,29,4
4
  1/22/2026,8,20,30,42,46,15
 
1
  date,b1,b2,b3,b4,b5,lucky_ball
2
+ 1/29/2026,14,24,25,39,40,17
3
+ 1/28/2026,19,24,26,27,47,14
4
+ 1/27/2026,1,10,32,37,48,9
5
+ 1/26/2026,3,21,22,42,44,9
6
+ 1/25/2026,2,25,27,29,31,13
7
  1/24/2026,8,17,25,40,44,7
8
  1/23/2026,6,16,17,18,29,4
9
  1/22/2026,8,20,30,42,46,15
gimme5_results.csv CHANGED
@@ -1,4 +1,8 @@
1
  Date,b1,b2,b3,b4,b5
 
 
 
 
2
  1/23/2026,4,5,13,26,32
3
  1/22/2026,16,19,22,23,27
4
  1/21/2026,3,9,13,14,20
 
1
  Date,b1,b2,b3,b4,b5
2
+ 1/29/2026,3,16,18,21,33
3
+ 1/28/2026,4,14,16,32,37
4
+ 1/27/2026,3,9,11,17,39
5
+ 1/26/2026,3,19,24,32,39
6
  1/23/2026,4,5,13,26,32
7
  1/22/2026,16,19,22,23,27
8
  1/21/2026,3,9,13,14,20
la_results.csv CHANGED
@@ -1,4 +1,6 @@
1
  date,b1,b2,b3,b4,b5,star_ball
 
 
2
  1/24/2026,4,11,16,33,42,6
3
  1/21/2026,11,30,39,48,51,4
4
  1/19/2026,2,10,15,18,31,9
 
1
  date,b1,b2,b3,b4,b5,star_ball
2
+ 1/28/2026,25,31,33,36,41,2
3
+ 1/26/2026,2,12,15,27,48,9
4
  1/24/2026,4,11,16,33,42,6
5
  1/21/2026,11,30,39,48,51,4
6
  1/19/2026,2,10,15,18,31,9
mb_results.csv CHANGED
@@ -1,4 +1,6 @@
1
  date,b1,b2,b3,b4,b5,megaball
 
 
2
  1/24/2026,6,13,24,31,37,5
3
  1/21/2026,12,13,39,40,41,6
4
  1/19/2026,18,21,36,37,38,1
 
1
  date,b1,b2,b3,b4,b5,megaball
2
+ 1/28/2026,8,11,23,28,37,4
3
+ 1/26/2026,10,12,26,30,37,2
4
  1/24/2026,6,13,24,31,37,5
5
  1/21/2026,12,13,39,40,41,6
6
  1/19/2026,18,21,36,37,38,1
mm_results.csv CHANGED
@@ -1,4 +1,5 @@
1
  date,b1,b2,b3,b4,b5,megaball
 
2
  1/23/2026,30,42,49,53,66,4
3
  1/20/2026,8,47,50,56,70,12
4
  1/16/2026,2,22,33,42,67,1
 
1
  date,b1,b2,b3,b4,b5,megaball
2
+ 1/27/2026,4,20,38,56,66,5
3
  1/23/2026,30,42,49,53,66,4
4
  1/20/2026,8,47,50,56,70,12
5
  1/16/2026,2,22,33,42,67,1
pb_results.csv CHANGED
@@ -1,4 +1,6 @@
1
  date,b1,b2,b3,b4,b5,powerball
 
 
2
  1/24/2026,2,16,35,61,63,5
3
  1/21/2026,11,26,27,53,55,12
4
  1/19/2026,5,28,34,37,55,17
 
1
  date,b1,b2,b3,b4,b5,powerball
2
+ 1/28/2026,21,35,40,46,68,11
3
+ 1/26/2026,21,31,51,60,63,18
4
  1/24/2026,2,16,35,61,63,5
5
  1/21/2026,11,26,27,53,55,12
6
  1/19/2026,5,28,34,37,55,17
predictor.py ADDED
@@ -0,0 +1,528 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ predictor.py — Universal RandomForest (CSV-only) predictor for lottery-style games.
4
+
5
+ What it does
6
+ - Reads a game's CSV draw history directly (no dependence on your engine pools).
7
+ - Trains RandomForestClassifier models to estimate the probability each number will appear next draw.
8
+ - Produces:
9
+ * RF-1: Top-N numbers by probability (most likely)
10
+ * RF-2: Diversified RF (sampled from top pool; differs from RF-1 by >=2 numbers by default)
11
+
12
+ Designed to be robust across many CSV formats:
13
+ - Columns like n1..n5 / num1..num5 / ball1..ball5
14
+ - Any 5 numeric columns (fallback)
15
+ - A single column containing a hyphen/space/comma separated list of numbers (fallback heuristic)
16
+
17
+ Optional bonus support
18
+ - If a bonus column exists (e.g., megaball/powerball/starball), you can pass bonus_max and it will
19
+ also train a bonus RF and return bonus prediction.
20
+
21
+ Requirements
22
+ - pandas
23
+ - scikit-learn
24
+ - numpy
25
+
26
+ If scikit-learn isn't installed, this module returns None predictions (safe failure).
27
+
28
+ Usage (import)
29
+ from predictor import UniversalRFPredictor
30
+ rf = UniversalRFPredictor()
31
+ out = rf.predict(csv_path, main_max=52, main_n=5, bonus_max=10, bonus_n=1)
32
+ print(out["rf1_numbers"], out.get("rf1_bonus"))
33
+ print(out["rf2_numbers"], out.get("rf2_bonus"))
34
+
35
+ Usage (CLI)
36
+ python predictor.py --csv "E:\data\la_results.csv" --main-max 52 --main-n 5 --bonus-max 10 --bonus-n 1
37
+ """
38
+
39
+ from __future__ import annotations
40
+
41
+ import argparse
42
+ import hashlib
43
+ import os
44
+ import re
45
+ import random
46
+ from dataclasses import dataclass
47
+ from typing import Any, Dict, List, Optional, Sequence, Tuple
48
+
49
+ # ----------------------------- helpers -----------------------------
50
+
51
+ _SPLIT_RE = re.compile(r"[^0-9]+")
52
+
53
+ def _safe_int(x: Any) -> Optional[int]:
54
+ try:
55
+ if x is None:
56
+ return None
57
+ if isinstance(x, bool):
58
+ return None
59
+ return int(x)
60
+ except Exception:
61
+ try:
62
+ s = str(x).strip()
63
+ if not s:
64
+ return None
65
+ return int(float(s))
66
+ except Exception:
67
+ return None
68
+
69
+ def _dedupe(seq: Sequence[Any]) -> List[int]:
70
+ out: List[int] = []
71
+ seen = set()
72
+ for v in (seq or []):
73
+ iv = _safe_int(v)
74
+ if iv is None:
75
+ continue
76
+ if iv in seen:
77
+ continue
78
+ seen.add(iv)
79
+ out.append(iv)
80
+ return out
81
+
82
+ def _md5_seed(s: str, fallback: int = 1337) -> int:
83
+ try:
84
+ return int(hashlib.md5(s.encode("utf-8")).hexdigest()[:8], 16)
85
+ except Exception:
86
+ return fallback
87
+
88
+ def _clip(nums: Sequence[int], lo: int, hi: int) -> List[int]:
89
+ out = []
90
+ for n in nums:
91
+ if lo <= n <= hi:
92
+ out.append(n)
93
+ return out
94
+
95
+ # ----------------------------- CSV parsing -----------------------------
96
+
97
+ @dataclass
98
+ class ParsedHistory:
99
+ draws: List[List[int]] # list of main number lists
100
+ bonus: List[Optional[int]] # list of bonus values aligned with draws
101
+
102
+ class CSVHistoryReader:
103
+ """Best-effort reader for many draw-history CSV layouts."""
104
+
105
+ DEFAULT_MAIN_PATTERNS = [
106
+ ["n1", "n2", "n3", "n4", "n5"],
107
+ ["num1", "num2", "num3", "num4", "num5"],
108
+ ["ball1", "ball2", "ball3", "ball4", "ball5"],
109
+ ["w1", "w2", "w3", "w4", "w5"],
110
+ ]
111
+
112
+ DEFAULT_BONUS_CANDIDATES = [
113
+ "mb", "megaball", "mega_ball",
114
+ "pb", "powerball", "power_ball",
115
+ "sb", "star", "starball", "star_ball",
116
+ "bonus", "bonusball", "bonus_ball",
117
+ "luckyball", "lucky_ball",
118
+ ]
119
+
120
+ def __init__(self, verbose: bool = False):
121
+ self.verbose = verbose
122
+
123
+ def read(self, csv_path: str, main_n: int = 5) -> ParsedHistory:
124
+ try:
125
+ import pandas as pd # type: ignore
126
+ except Exception:
127
+ return ParsedHistory(draws=[], bonus=[])
128
+
129
+ try:
130
+ df = pd.read_csv(csv_path)
131
+ except Exception:
132
+ return ParsedHistory(draws=[], bonus=[])
133
+
134
+ # 1) Find main columns using common patterns
135
+ main_cols: Optional[List[str]] = None
136
+ for pat in self.DEFAULT_MAIN_PATTERNS:
137
+ if all(c in df.columns for c in pat[:main_n]):
138
+ main_cols = pat[:main_n]
139
+ break
140
+
141
+ # 2) If not found, choose first `main_n` numeric columns
142
+ if main_cols is None:
143
+ numeric_cols = []
144
+ for c in df.columns:
145
+ try:
146
+ kind = getattr(df[c].dtype, "kind", "")
147
+ except Exception:
148
+ kind = ""
149
+ if kind in ("i", "u", "f"):
150
+ numeric_cols.append(c)
151
+ if len(numeric_cols) >= main_n:
152
+ main_cols = numeric_cols[:main_n]
153
+
154
+ # 3) If still not found, look for a single column that contains a list of numbers
155
+ list_col: Optional[str] = None
156
+ if main_cols is None:
157
+ for c in df.columns:
158
+ if df[c].dtype == object:
159
+ # check if it looks like "1-2-3-4-5" or "1 2 3 4 5"
160
+ sample = df[c].dropna().astype(str).head(10).tolist()
161
+ hits = 0
162
+ for s in sample:
163
+ parts = [p for p in _SPLIT_RE.split(s) if p]
164
+ if len(parts) >= main_n and all(p.isdigit() for p in parts[:main_n]):
165
+ hits += 1
166
+ if hits >= max(1, len(sample) // 2):
167
+ list_col = c
168
+ break
169
+
170
+ # Bonus column detection
171
+ bonus_col: Optional[str] = None
172
+ lower_cols = {str(c).lower(): c for c in df.columns}
173
+ for cand in self.DEFAULT_BONUS_CANDIDATES:
174
+ if cand in lower_cols:
175
+ bonus_col = lower_cols[cand]
176
+ break
177
+
178
+ draws: List[List[int]] = []
179
+ bonus: List[Optional[int]] = []
180
+
181
+ if main_cols is not None:
182
+ for _, row in df[main_cols].iterrows():
183
+ nums = []
184
+ ok = True
185
+ for c in main_cols:
186
+ iv = _safe_int(row[c])
187
+ if iv is None:
188
+ ok = False
189
+ break
190
+ nums.append(iv)
191
+ if ok and len(nums) == main_n:
192
+ draws.append(nums)
193
+ bonus.append(_safe_int(row[bonus_col]) if bonus_col else None)
194
+
195
+ elif list_col is not None:
196
+ for _, row in df[[list_col]].iterrows():
197
+ s = str(row[list_col])
198
+ parts = [p for p in _SPLIT_RE.split(s) if p]
199
+ if len(parts) < main_n:
200
+ continue
201
+ nums = []
202
+ ok = True
203
+ for p in parts[:main_n]:
204
+ iv = _safe_int(p)
205
+ if iv is None:
206
+ ok = False
207
+ break
208
+ nums.append(iv)
209
+ if ok and len(nums) == main_n:
210
+ draws.append(nums)
211
+ # bonus may also be embedded later; ignore here (None)
212
+ bonus.append(None)
213
+ else:
214
+ # Could not parse
215
+ return ParsedHistory(draws=[], bonus=[])
216
+
217
+ return ParsedHistory(draws=draws, bonus=bonus)
218
+
219
+ # ----------------------------- RF core -----------------------------
220
+
221
+ class UniversalRFPredictor:
222
+ def __init__(self, verbose: bool = False):
223
+ self.verbose = verbose
224
+ self.reader = CSVHistoryReader(verbose=verbose)
225
+
226
+ def _build_features(self, draws: List[List[int]], universe_max: int, lookback: int = 12):
227
+ """Build per-number time-series features and next-step feature vector."""
228
+ import numpy as np # type: ignore
229
+
230
+ if len(draws) < (lookback + 5):
231
+ return {}
232
+
233
+ appears = {n: [0] * len(draws) for n in range(1, universe_max + 1)}
234
+ for t, d in enumerate(draws):
235
+ s = set(d)
236
+ for n in s:
237
+ if 1 <= n <= universe_max:
238
+ appears[n][t] = 1
239
+
240
+ def recent_count(arr, t, w):
241
+ return int(sum(arr[max(0, t - w):t]))
242
+
243
+ def gap_since(arr, t):
244
+ for k in range(1, t + 1):
245
+ if arr[t - k] == 1:
246
+ return k
247
+ return t
248
+
249
+ feats = {}
250
+ for n in range(1, universe_max + 1):
251
+ arr = appears[n]
252
+ X, y = [], []
253
+ for t in range(lookback, len(draws)):
254
+ f = [
255
+ recent_count(arr, t, 5),
256
+ recent_count(arr, t, 10),
257
+ gap_since(arr, t),
258
+ arr[t - 1],
259
+ arr[t - 2] if t - 2 >= 0 else 0,
260
+ arr[t - 3] if t - 3 >= 0 else 0,
261
+ ]
262
+ X.append(f)
263
+ y.append(arr[t])
264
+
265
+ t = len(draws)
266
+ last_f = [
267
+ recent_count(arr, t, 5),
268
+ recent_count(arr, t, 10),
269
+ gap_since(arr, t),
270
+ arr[t - 1],
271
+ arr[t - 2] if t - 2 >= 0 else 0,
272
+ arr[t - 3] if t - 3 >= 0 else 0,
273
+ ]
274
+ feats[n] = (np.asarray(X, float), np.asarray(y, int), np.asarray(last_f, float))
275
+ return feats
276
+
277
+ def _rank_numbers(self, draws: List[List[int]], universe_max: int, seed: int) -> List[Tuple[int, float]]:
278
+ try:
279
+ from sklearn.ensemble import RandomForestClassifier # type: ignore
280
+ import numpy as np # type: ignore
281
+ except Exception:
282
+ return []
283
+
284
+ feats = self._build_features(draws, universe_max, lookback=12)
285
+ if not feats:
286
+ return []
287
+
288
+ probs: List[Tuple[int, float]] = []
289
+ for n, (X, y, last_f) in feats.items():
290
+ if int(y.sum()) < 5 or int((1 - y).sum()) < 5:
291
+ continue
292
+ try:
293
+ clf = RandomForestClassifier(
294
+ n_estimators=240,
295
+ max_depth=9,
296
+ random_state=seed,
297
+ class_weight="balanced",
298
+ n_jobs=-1
299
+ )
300
+ clf.fit(X, y)
301
+ p = float(clf.predict_proba(last_f.reshape(1, -1))[0][1])
302
+ probs.append((n, p))
303
+ except Exception:
304
+ continue
305
+
306
+ probs.sort(key=lambda t: t[1], reverse=True)
307
+ return probs
308
+
309
+ def _rank_bonus(self, bonus_series: List[Optional[int]], bonus_max: int, seed: int) -> List[Tuple[int, float]]:
310
+ """Simple RF for bonus (single categorical per draw)."""
311
+ try:
312
+ from sklearn.ensemble import RandomForestClassifier # type: ignore
313
+ import numpy as np # type: ignore
314
+ except Exception:
315
+ return []
316
+
317
+ # Need enough bonus observations
318
+ b = [_safe_int(x) for x in bonus_series]
319
+ if sum(1 for x in b if x is not None) < 40:
320
+ return []
321
+
322
+ # Build appearance series for each bonus value 1..bonus_max
323
+ T = len(b)
324
+
325
+ def recent_count(arr, t, w):
326
+ return int(sum(arr[max(0, t - w):t]))
327
+
328
+ def gap_since(arr, t):
329
+ for k in range(1, t + 1):
330
+ if arr[t - k] == 1:
331
+ return k
332
+ return t
333
+
334
+ probs: List[Tuple[int, float]] = []
335
+ for val in range(1, bonus_max + 1):
336
+ arr = [1 if _safe_int(b[t]) == val else 0 for t in range(T)]
337
+ X, y = [], []
338
+ lookback = 10
339
+ for t in range(lookback, T):
340
+ f = [
341
+ recent_count(arr, t, 5),
342
+ recent_count(arr, t, 10),
343
+ gap_since(arr, t),
344
+ arr[t - 1],
345
+ arr[t - 2] if t - 2 >= 0 else 0,
346
+ arr[t - 3] if t - 3 >= 0 else 0,
347
+ ]
348
+ X.append(f)
349
+ y.append(arr[t])
350
+ if len(X) < 30:
351
+ continue
352
+ X = np.asarray(X, float)
353
+ y = np.asarray(y, int)
354
+ if int(y.sum()) < 3 or int((1 - y).sum()) < 3:
355
+ continue
356
+ last_f = np.asarray([
357
+ recent_count(arr, T, 5),
358
+ recent_count(arr, T, 10),
359
+ gap_since(arr, T),
360
+ arr[T - 1],
361
+ arr[T - 2] if T - 2 >= 0 else 0,
362
+ arr[T - 3] if T - 3 >= 0 else 0,
363
+ ], float).reshape(1, -1)
364
+
365
+ try:
366
+ clf = RandomForestClassifier(
367
+ n_estimators=200,
368
+ max_depth=8,
369
+ random_state=seed,
370
+ class_weight="balanced",
371
+ n_jobs=-1
372
+ )
373
+ clf.fit(X, y)
374
+ p = float(clf.predict_proba(last_f)[0][1])
375
+ probs.append((val, p))
376
+ except Exception:
377
+ continue
378
+
379
+ probs.sort(key=lambda t: t[1], reverse=True)
380
+ return probs
381
+
382
+ def _pick_rf1_rf2(self, probs: List[Tuple[int, float]], main_n: int, min_diff: int = 2) -> Tuple[List[int], Optional[List[int]]]:
383
+ if not probs or len(probs) < main_n:
384
+ return [], None
385
+
386
+ rf1 = [n for n, _ in probs[:main_n]]
387
+
388
+ # diversified sampling from top pool
389
+ pool = probs[:max(12, main_n * 3)]
390
+ nums = [n for n, _ in pool]
391
+ weights = [max(1e-9, p) for _, p in pool]
392
+
393
+ seed = _md5_seed(",".join(map(str, rf1)) + "|" + ",".join(map(str, nums[:10])))
394
+ rng = random.Random(seed)
395
+
396
+ def sample_ticket() -> List[int]:
397
+ chosen = []
398
+ remaining = list(zip(nums, weights))
399
+ for _ in range(main_n):
400
+ total = sum(w for _, w in remaining)
401
+ if total <= 0:
402
+ break
403
+ r = rng.random() * total
404
+ cum = 0.0
405
+ pick_idx = 0
406
+ for i, (n, w) in enumerate(remaining):
407
+ cum += w
408
+ if cum >= r:
409
+ pick_idx = i
410
+ break
411
+ n_pick, _ = remaining.pop(pick_idx)
412
+ chosen.append(n_pick)
413
+ return sorted(_dedupe(chosen))
414
+
415
+ rf2: Optional[List[int]] = None
416
+ for _ in range(60):
417
+ cand = sample_ticket()
418
+ if len(cand) != main_n:
419
+ continue
420
+ overlap = len(set(cand) & set(rf1))
421
+ # require at least `min_diff` numbers different
422
+ if overlap <= (main_n - min_diff):
423
+ rf2 = cand
424
+ break
425
+
426
+ # fallback: if diversification fails, return None (honest)
427
+ return sorted(rf1), sorted(rf2) if rf2 else None
428
+
429
+ def predict(
430
+ self,
431
+ csv_path: str,
432
+ main_max: int,
433
+ main_n: int = 5,
434
+ bonus_max: Optional[int] = None,
435
+ bonus_n: int = 0,
436
+ seed_key: str = "",
437
+ min_diff: int = 2,
438
+ min_draws: int = 60,
439
+ ) -> Dict[str, Any]:
440
+ """Return RF predictions from CSV history."""
441
+ out: Dict[str, Any] = {
442
+ "ok": False,
443
+ "rf1_numbers": [],
444
+ "rf2_numbers": None,
445
+ "rf1_bonus": None,
446
+ "rf2_bonus": None,
447
+ "reason": "",
448
+ }
449
+
450
+ if not csv_path or not os.path.exists(csv_path):
451
+ out["reason"] = "csv_missing"
452
+ return out
453
+
454
+ hist = self.reader.read(csv_path, main_n=main_n)
455
+ draws = hist.draws
456
+ if len(draws) < min_draws:
457
+ out["reason"] = f"too_few_draws:{len(draws)}"
458
+ return out
459
+
460
+ # clip to valid range defensively
461
+ draws = [_clip(_dedupe(d), 1, int(main_max))[:main_n] for d in draws if d]
462
+ draws = [d for d in draws if len(d) == main_n]
463
+ if len(draws) < min_draws:
464
+ out["reason"] = f"too_few_valid_draws:{len(draws)}"
465
+ return out
466
+
467
+ seed = _md5_seed(seed_key or (csv_path + "|" + str(main_max) + "|" + str(main_n)))
468
+ probs = self._rank_numbers(draws, int(main_max), seed=seed)
469
+ if not probs:
470
+ out["reason"] = "rf_rank_empty"
471
+ return out
472
+
473
+ rf1, rf2 = self._pick_rf1_rf2(probs, main_n=int(main_n), min_diff=int(min_diff))
474
+ if not rf1:
475
+ out["reason"] = "rf1_empty"
476
+ return out
477
+
478
+ out["rf1_numbers"] = rf1
479
+ out["rf2_numbers"] = rf2
480
+ out["ok"] = True
481
+
482
+ # Bonus prediction (optional)
483
+ if bonus_max and bonus_n and bonus_n > 0:
484
+ bprobs = self._rank_bonus(hist.bonus, int(bonus_max), seed=seed)
485
+ if bprobs:
486
+ out["rf1_bonus"] = bprobs[0][0]
487
+ # For bonus, "diversification" isn't meaningful; if rf2 exists, reuse rf1_bonus
488
+ out["rf2_bonus"] = out["rf1_bonus"]
489
+
490
+ return out
491
+
492
+
493
+ # ----------------------------- CLI -----------------------------
494
+
495
+ def main():
496
+ p = argparse.ArgumentParser(description="Universal RF predictor (CSV-only).")
497
+ p.add_argument("--csv", required=True, help="Path to draw-history CSV")
498
+ p.add_argument("--main-max", required=True, type=int, help="Max main number (e.g., 52)")
499
+ p.add_argument("--main-n", default=5, type=int, help="Count of main numbers per draw")
500
+ p.add_argument("--bonus-max", default=None, type=int, help="Max bonus number (optional)")
501
+ p.add_argument("--bonus-n", default=0, type=int, help="Bonus count (0 or 1)")
502
+ p.add_argument("--min-draws", default=60, type=int, help="Minimum draws required")
503
+ p.add_argument("--min-diff", default=2, type=int, help="RF-2 min number-diff vs RF-1")
504
+ p.add_argument("--seed-key", default="", help="Seed key for reproducibility")
505
+ p.add_argument("--verbose", action="store_true")
506
+ args = p.parse_args()
507
+
508
+ rf = UniversalRFPredictor(verbose=args.verbose)
509
+ out = rf.predict(
510
+ csv_path=args.csv,
511
+ main_max=args.main_max,
512
+ main_n=args.main_n,
513
+ bonus_max=args.bonus_max,
514
+ bonus_n=args.bonus_n,
515
+ seed_key=args.seed_key,
516
+ min_diff=args.min_diff,
517
+ min_draws=args.min_draws,
518
+ )
519
+ print(out)
520
+
521
+ if out.get("ok"):
522
+ print("RF-1:", out["rf1_numbers"], "BONUS:", out.get("rf1_bonus"))
523
+ print("RF-2:", out.get("rf2_numbers"), "BONUS:", out.get("rf2_bonus"))
524
+ else:
525
+ print("Not OK:", out.get("reason"))
526
+
527
+ if __name__ == "__main__":
528
+ main()