Skip to content

Repository files navigation

Lotto Analyzer

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.

What this does (and does not do)

This tool does two real things:

  1. It describes what actual historical draws look like. Sums, spacing, odd/even split, decade spread, gaps, jackpot stats, and so on.
  2. 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.

Project layout

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 web app

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.sh

It 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.py

Five games, one app

The 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-42Lotto 6/42 (numbers 1–42).
  • /Megalotto 6/45 (numbers 1–45), the default page.
  • /6-49Superlotto 6/49 (numbers 1–49).
  • /6-55Grand Lotto 6/55 (numbers 1–55).
  • /6-58Ultra 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).

The All page (/all)

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.

The four tabs (per game)

  • 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.py writes.
  • 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.

Adding new draws from the browser

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.

Fetching official results from PCSO

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 chromium

If 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.)

Quick start

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.py

If there is no file in data/, the tool runs on synthetic fair-random draws so you can test everything before adding your real history.

Commands

# 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 44

A 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.

Tuning

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_000

Change a value, rerun, compare. Nothing is hidden in the code.

How the score works

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_WINDOW draws.
  • 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.

Validating the score (validate.py)

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 section

What 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.

Per-game calibration fix

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.

A note on what we deliberately avoid

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.


GitHub

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 account

push.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

Working on it in Cursor with Claude Code

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.

  1. Open the Lotto folder in Cursor (File, Open Folder).

  2. 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 | bash

    Windows PowerShell:

    irm https://claude.ai/install.ps1 | iex

    Close and reopen the terminal afterward, then check it with claude --version. (The older npm install -g @anthropic-ai/claude-code method still works but needs Node.js 18+ and is no longer the primary path.)

  3. Open Cursor's integrated terminal (View, Terminal), make sure you are in the project folder, and run:

    claude
  4. 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

About

Honest statistics + web app for PCSO lotto games (6/42, 6/45, 6/49, 6/55, 6/58): historical analysis, anti-human ticket scoring, and one-click official-results fetch.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages