A small, honest statistics project for PCSO 6-number lotto draw history, covering Lotto 6/42, Megalotto 6/45, Superlotto 6/49, Grand Lotto 6/55, and Ultra Lotto 6/58.
This tool does two real things:
- It describes what actual historical draws look like. Sums, spacing, odd/even split, decade spread, gaps, jackpot stats, and so on.
- It scores candidate tickets so it prefers ones that look like authentic draws and are unlikely to be popular human picks (birthdays, sequences, patterns on the slip).
What it does not do: predict the next draw or improve your odds of winning. A fair draw is independent every time, so no past data changes the next result. The only part with a plausible real payoff is the anti-human scoring. If you ever do win, an unpopular ticket is less likely to be split with other players. Treat any spending like a movie ticket, not an investment.
Lotto/
├── app.py # web app (Flask) — wraps the engine in a browser UI
├── pcso.py # fetches official results from PCSO (via Playwright)
├── run_web.sh # one-command launcher for the web app
├── Lotto.command # double-click launcher (macOS)
├── push.sh # git push as jeflor across gh account switches
├── web/ # web front-end (index.html + app.js, all.html + all.js, style.css)
├── analyze.py # command line entry point
├── config.py # all tunable settings and scoring weights
├── loader.py # reads the Excel/CSV history (with synthetic fallback)
├── stats.py # historical analysis
├── fingerprint.py # turns a ticket into a feature vector
├── scoring.py # the Jeff Score (historical + recent + anti-human)
├── generator.py # generates and ranks random candidate tickets
├── reports.py # console summary, charts, CSV, per-draw report
├── test_basic.py # small sanity tests
├── requirements.txt
├── data/ # one committed draw-history CSV per game
└── output/ # charts and top_tickets.csv land here
The same engine, in your browser. Nothing here changes the analysis — app.py
is a thin Flask wrapper that calls the exact same scoring, stats, and
generator code as the command line.
# easiest: one command (creates .venv and installs deps on first run)
./run_web.shIt opens your browser automatically at http://127.0.0.1:5050. (Port 5050, not
5000: on macOS, Control Center's AirPlay Receiver also uses 5000, which makes it
flaky. Set PORT to change it.) Or do it by hand:
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
python app.pyThe web app serves several lottery games, sharing 100% of the engine and rules. A 6/42 | 6/45 | 6/49 | 6/55 | 6/58 switcher sits in the top-right corner:
/6-42— Lotto 6/42 (numbers 1–42)./— Megalotto 6/45 (numbers 1–45), the default page./6-49— Superlotto 6/49 (numbers 1–49)./6-55— Grand Lotto 6/55 (numbers 1–55)./6-58— Ultra Lotto 6/58 (numbers 1–58).
Only the number range and decade buckets change between games; the birthday
threshold, anti-human penalties, and scoring blend are identical. Adding another
game is just one entry in the GAMES registry in app.py, a matching route,
and (for PCSO fetching) one entry in pcso.py's GAME_VALUES.
Data files are matched to games by name: a file with 6-49/649 in its name
feeds 6/49, 6-55 feeds 6/55, and so on; a file matching no other game feeds the
default 6/45. Each game keeps its own CSV in data/.
There's also a ⟳ Update all from PCSO button in the header that refreshes every game at once (see the PCSO section below).
All sits at the end of the game switcher, alongside 6/42 … 6/58. It's a page rather than a tab because it isn't scoped to one game: it lists every game on a single screen, 6/42 at the top down to 6/58 at the bottom, each row showing the last stated jackpot (and whether it was won or rolled over), the last winning numbers in the order they were drawn, and the estimated next draw day and date. Click any row to open that game's analyzer. The ⟳ Update all from PCSO button works here too.
- Overview — a current-jackpot banner (the last stated jackpot, the winning
numbers in the order they were drawn, and an estimated next-draw date derived
from the game's cadence), then historical summary cards and live charts
(number frequency, sum distribution, odd/even split). A button also renders
the same matplotlib PNGs
analyze.pywrites. - Score a Ticket — type six numbers, get the full Jeff Score breakdown with the plain-language reasons.
- Generate Tickets — sample and rank candidates (capped at 300,000 in the browser so requests stay snappy; use the CLI for a full 1,000,000 run).
- Data & Settings — upload a full history, add new draws (see below), reload the file from disk, and adjust the three scoring weights live.
Jackpot figures on both the Overview tab and the All page are the prize stated for the last draw on file, not the live upcoming pot, and next-draw dates are estimated from each game's recent draw-day pattern rather than read from an official notice.
The old Draw Report tab (compare a played ticket against a drawn result) was
removed — counting matches on six numbers is something you do faster by eye, and
it implied the tool tracks played tickets, which it doesn't. The equivalent CLI
flag (analyze.py --played ... --drawn ...) still exists.
On Data & Settings, the Add draws section appends new results to the current game's CSV — no need to edit the file by hand or re-upload the whole history. Three ways:
- Fetch latest from PCSO — one button pulls official results from pcso.gov.ph (from the last draw on file through today) and appends anything new. See the next section.
- Latest draw — a guided form: draw date (defaults to today), six number boxes validated to the game's range, plus optional jackpot and winners.
- Or paste several — the parser reads each line by content, so you can paste
straight from a spreadsheet (tab-separated, with the game name and a comma'd
jackpot) or type shorthand like
date, n1 n2 n3 n4 n5 n6. Column order does not matter.
Draws already on file (same date and numbers) are skipped, so re-entering is safe. Malformed rows are reported line-by-line and nothing is saved until they are fixed. Appends are written atomically with a rollback guard, so a bad entry can never corrupt the data file.
The Fetch from PCSO button scrapes the official results page and appends new draws automatically. By default it fetches from the last draw already on file through today, so repeat clicks just top up what's missing (duplicates are skipped). You can also set an explicit From/To range — handy for backfilling an older gap — and leave either bound blank to keep the default for that end.
PCSO's site sits behind Akamai bot protection that blocks ordinary HTTP requests
(they get 403 Access Denied), so pcso.py drives a real browser via
Playwright instead: it opens the results page, sets the date range and game,
clicks Search, and reads the table (whose columns already match the CSV). By
default it runs headful — a browser window opens briefly — because headless
traffic is what Akamai most often blocks; set PCSO_HEADLESS=1 to try headless.
Playwright and its browser are installed automatically on first run by
run_web.sh / Lotto.command. To set it up by hand:
pip install playwright
playwright install chromiumIf PCSO changes their page or blocks the browser, the button reports the problem and the paste/manual entry methods above keep working as a fallback.
This runs locally on your machine only. GitHub's free hosting (GitHub Pages) is static-only and cannot run Python, so the web app is not something you publish there — you push the code to your GitHub and run it locally. (See the GitHub section below.)
You need Python 3.10 or newer.
# 1. (optional) create a virtual environment
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
# 2. install dependencies
pip install -r requirements.txt
# 3. drop your data file in
# copy "6-45 draws.xlsx" into the data/ folder
# 4. run it
python analyze.pyIf there is no file in data/, the tool runs on synthetic fair-random draws so
you can test everything before adding your real history.
# full run, 1,000,000 candidates (slowest, most thorough)
python analyze.py
# faster run with fewer candidates
python analyze.py --candidates 200000
# show and save more top tickets
python analyze.py --top 25
# skip charts
python analyze.py --no-charts
# score one specific ticket and see why
python analyze.py --score 8 17 29 34 39 45
# compare one of your tickets against an actual draw result
# format is: your ticket / the drawn numbers
python analyze.py --report 8 17 29 34 39 45 / 3 12 20 31 38 44A full 1,000,000-candidate run is pure Python and takes a couple of minutes.
Start with --candidates 200000 while you tune things, then do a big run once
you are happy with the weights.
Open config.py. The interesting knobs:
HIGH_THRESHOLD = 31 # birthday boundary, used for low/high counts
RECENT_WINDOW = 25 # how many draws count as "recent"
HISTORICAL_WEIGHT = 0.40 # looks like a typical all-time draw
RECENT_WEIGHT = 0.20 # looks like the last RECENT_WINDOW draws
ANTI_HUMAN_WEIGHT = 0.40 # unlikely to be a popular human pick
DEFAULT_CANDIDATE_COUNT = 1_000_000Change a value, rerun, compare. Nothing is hidden in the code.
Every ticket is turned into a fingerprint: a vector of structural features (sum, mean, median, spread, range, largest and smallest gap, entropy, odd count, high count, consecutive pairs, decade counts). The same fingerprint is computed for every historical draw.
- Historical similarity: how structurally typical the ticket is, scored against every real draw. More typical scores higher.
- Recent similarity: the same, but against only the last
RECENT_WINDOWdraws. - Human uniqueness: starts at 100 and subtracts penalties for birthday-only tickets, long runs, arithmetic sequences, too many multiples of five, same-last-digit slip patterns, extreme sums, and so on. Every penalty is reported in plain language, so you can see exactly why a ticket scored what it did.
Overall score is a weighted blend of the three.
The claims above should not be taken on faith, and the winner-count column in the data lets us test the main one. Run:
./.venv/bin/python validate.py # all sections, ~2 minutes
./.venv/bin/python validate.py 1 # just one sectionWhat it found (all five games, 7,799 draws, 575 of them jackpot hits):
1. Anti-human scoring is real, and small. Because the draw is random, which
combination gets a given anti-human score is random too — a natural experiment.
The winner count then says how many people had actually picked it.
corr(anti_human_score, winner_count) = -0.20, 95% CI [-0.224, -0.182],
essentially unchanged when controlling for jackpot size. Negative is the
predicted direction: combos that look like human picks get picked by more people.
Within every game, combos carrying human-pick tells were hit 3.3x–18.6x
more often than clean ones. The payoff lives in the tail: on 2022-10-01 the 6/55
draw was 09-18-27-36-45-54 — multiples of nine, a perfect arithmetic sequence —
and it was split 433 ways. A typical winning draw has one winner. The score
flagged it.
2. The three components do not fight — they duplicate. The worry was that
"structurally typical" pulls against "unlikely to be a human pick". It does not:
r(historical, anti-human) ≈ +0.5. The real problem is next door —
r(historical, recent) = +0.82 to +0.95. Historical and recent similarity are
very nearly the same measurement, so the 40% + 20% is 60% on one idea, twice.
3. Anti-human is a filter, not a ranker. 61–71% of random tickets score the
maximum, and the scale only takes 16–20 distinct values. So ranking happens
entirely inside that tie group, decided by historical/recent similarity —
full and anti_human_only share 0 of their top 10.
4. The tie-break does nothing measurable. Inside the tie group, on real
draws, corr(historical_similarity, winner_count) = -0.006, CI [-0.033, +0.021], n=5,227. That is a well-powered null, not a shrug: structural
typicality neither raises nor lowers how many people share a jackpot. It is not
harmful; it is simply not worth 60% of a score.
Use SCORING_MODE in config.py ("full" or "anti_human_only") to rank on
anti-human alone and compare for yourself.
The anti-human penalties had two constants hardcoded for 6/45: the "balanced sum"
band (100–170) and the midpoint (23) in the symmetric-pattern check. A typical
6/58 draw sums to ~177, so the fixed band flagged nearly every 6/58 draw as an
"extreme sum" — a penalty that fires on everything carries no information. Both
now derive from each game's own number range (SUM_WINDOW_SDS in config.py),
which reproduces the original 6/45 band (~101–175) while behaving correctly for
6/42 through 6/58.
No hot numbers, cold numbers, overdue numbers, cycle prediction, or "the machine likes 37." Over ~1,570 fair draws, individual number frequencies are mostly random noise. Leaning on them would fake a prediction the data does not support.
This lives at https://github.com/jeflor/lotto. To push changes (new draws,
tweaks) from inside the Lotto folder:
git add -A
git commit -m "describe what changed"
./push.sh # pushes as jeflor, then restores the previous gh accountpush.sh exists because the gh "active account" is machine-global: if another
project switches it away from jeflor, a plain git push here 403s. The script
flips to jeflor just for the push and restores the prior account afterward. (A
plain git push still works whenever jeflor is already the active account.)
The draw-history CSVs in data/ are committed so the repo is usable on clone.
If you would rather keep your data private, uncomment the data/*.csv and
data/*.xlsx lines in .gitignore (and git rm --cached data/*.csv to stop
tracking the existing ones).
GitHub's own short guide is here: https://docs.github.com/en/get-started/quickstart/create-a-repo
Cursor (https://cursor.com) is a VS Code based editor. Claude Code is Anthropic's command line coding tool, and it runs fine inside Cursor's built-in terminal.
-
Open the
Lottofolder in Cursor (File, Open Folder). -
Install Claude Code. The native installer is the current recommended method and needs no Node.js:
macOS, Linux, or WSL:
curl -fsSL https://claude.ai/install.sh | bashWindows PowerShell:
irm https://claude.ai/install.ps1 | iex
Close and reopen the terminal afterward, then check it with
claude --version. (The oldernpm install -g @anthropic-ai/claude-codemethod still works but needs Node.js 18+ and is no longer the primary path.) -
Open Cursor's integrated terminal (View, Terminal), make sure you are in the project folder, and run:
claude
-
The first run opens a browser to sign in. You need a Claude Pro or Max plan, or an API account billed per token.
From there you can ask Claude Code to change the scoring, add charts, speed up the generator with NumPy, or anything else, and it will edit the files in place. Commit and push as you go with normal git.
Claude Code docs: https://docs.claude.com/en/docs/claude-code/overview