On this page12 sections
HitungKerusi User Guide
Last updated: 2026-10-19 (public guide de-operationalised: infra identifiers removed, reader view only)
How to read the forecast site and how to operate the pipeline. The mathematical specification behind everything here is the methodology paper.
1. What this site is
HitungKerusi forecasts Malaysia's 222 Dewan Rakyat seats ("kerusi") and five state assemblies (Melaka, Sarawak, Johor, Negeri Sembilan, Sabah). Nightly — and more often during election campaigns — the platform collects political news, re-checks its electoral database, and publishes a new forecast: per-seat win probabilities for each bloc (PH, BN, PN, GPS, GRS, WARISAN, Others), seat-count ranges per bloc, and headline probabilities. The public site at hitungkerusi.fyi renders each page straight from the platform's live database — a completed update publishes itself on the next page view, with no deployment step. Forecasts are estimates with uncertainty, not predictions of certainty — every screen shows the ranges, and the methodology page shows exactly how the numbers are made.
The site is bilingual — English and Bahasa Malaysia. Every page has its own address (/ms/… for BM), the EN | BM toggle in the header switches and remembers your choice, and first-time visitors get their language from browser settings. The interface, the guide, the methodology paper, data sources and the changelog are all translated. The site is also themable: it follows your device's light/dark setting by default, and the header toggle (Auto → Light → Dark) overrides and remembers it.
Navigation is two-level: Overview (the landing page), Parliament, States, Seats, Movement, Report, and About (which gathers the Guide, Methodology, Data sources and What's-new pages). Picking a section reveals its own submenu row — the five state assemblies under States, chamber scopes under Seats and Movement.
2. Reading the site
The Overview landing opens with the current story in one line — a computed verdict of the parliamentary race ("no bloc can govern alone", "PN likely largest"…) with the 10,000-simulation count beneath — then a countdown banner for the next scheduled election (days to polling, campaign progress), one tile per chamber showing its leader and seat share, and the latest weekly report headline. The full Parliament dashboard lives one click deeper, and every state assembly has the same dashboard shape.
On a chamber dashboard:
- The hemicycle is the chamber: one dot per seat, government blocs seated left in contiguous bands. Each dot is coloured by the seat's most-likely winning bloc, and the gold line marks the majority threshold — half the seats plus one, drawn at the same place whatever the result, so you can see at a glance whether the government side crosses it. Faded dots with a grey ring are toss-ups — seats where the leading bloc is below 60% win probability. Hover any dot for its code and probability; click to open the seat page.
- Seats by bloc shows each bloc's median seat count with its 80% interval in brackets — e.g.
87 [84–91]means half of simulations land at 87 or fewer, and 80% land between 84 and 91 — plus the movement since the current composition ("now"). - Metric cards: P(gov ≥ majority) — the probability the governing arrangement retains a working majority; P(hung) — probability no single bloc reaches majority (Malaysia's coalitions make this the norm, not a crisis); the two-thirds line where it applies; largest bloc — who most likely holds the most seats.
- Who can form the government? ranks the plausible coalition scenarios by their probability of reaching the majority (see §5).
3. Reading a seat
The seat explorer covers every chamber — 222 parliamentary constituencies plus all five state assemblies, switchable from the submenu (or the States pages). Default sort is most competitive first (smallest margin). Filter by state or leaning bloc; click any column to sort. "Margin" is the gap between the top two blocs' expected vote shares — single digits mean a knife-edge seat (36 seats were decided by under 5 points at GE15, which is why small national swings move many seats).
A seat page groups the constituency's actual results per election — GE12 through GE15 for federal seats, each state's PRNs, by-elections — newest first, with per-election vote totals, winner badges and candidate names (older elections are collapsed behind a summary line). Beneath the history: the current forecast (expected share + win probability per bloc), declared candidates for a coming election as nominations land, and recent news tagged to that seat by the cycle.
4. State assemblies
The States section holds chamber-specific forecasts for five assemblies. Melaka (28 seats, majority 15) is live for the PRN on 14 November 2026 — the campaign forecast, with declared candidates on seat pages as they are announced. Sarawak (82 seats, majority 42) is due by February 2027. Johor (56), Negeri Sembilan (36) and Sabah (73) held their PRNs in 2025–2026, so their pages forecast the next cycle (~2030–2031) — long-horizon bands, by design. Each state page shows its own hemicycle, intervals, probabilities and map, plus a campaign banner with the baseline result the forecast builds on. State baselines are single-election (each state's latest PRN), so during campaigns these forecasts move with the news layer — read the ranges.
Election links in the main menu: whenever a state election or by-election is announced (a dated row in the elections table) or a state's PRN comes within ~4 months of its due date, the menu bar carries a direct link to it — an amber pill for announced elections ("Melaka PRN · 28d"), a quieter green pill for due-date outlooks ("Sarawak PRN · 106d"). By-election pills link straight to the seat's page. The links appear and disappear by the calendar (via /api/v1/elections/upcoming, snapshot-backed), so a snap by-election shows up the moment its election row is entered.
5. Movement and government formation
The Movement page charts each bloc's median seat count across recent immutable runs — the platform's honest memory of how the forecast evolved, one dated input at a time. Every dashboard also carries a "Who can form the government?" card: the probability each plausible coalition reaches the majority across the 10,000 simulated outcomes — pure arithmetic on the seat maps, not an endorsement.
6. The weekly report
The Report page narrates the latest run per chamber — headline numbers, bloc intervals, and the seats whose win probabilities moved most since the previous run — generated deterministically from immutable runs (the same inputs always produce the same words). During campaigns this is the fastest way to see what a week of news actually moved.
7. Interpreting the numbers responsibly
- A 70% favourite loses three times in ten. Watch the ranges, not just the medians.
- Interval width scales with time-to-election. Uncertainty grows with the horizon (a damped random walk — methodology §9): the Melaka PRN a month out shows a tight band, while the parliament forecast at its 2027-2028 deadline spans 15+ seats. Long-horizon bands are still somewhat optimistic in width — the GE14→GE15 replay reached 43% coverage of the nominal 80% — so treat far-out ranges as a floor on the true uncertainty.
- The notional baseline means bloc labels reflect today's coalitions — history is re-bucketed onto current blocs (a 2018 PPBM vote counts for PN today), so historical bars and forecasts are always comparable.
- "Hung parliament" here means no single bloc reaches 112. Post-2022 Malaysian governments are multi-bloc by construction; the government-majority metric tracks the incumbent PH+BN arrangement.
8. Operating the pipeline
Everything is a make target or an hk command (full details in the development guide):
| What you want | Command |
|---|---|
| Refresh news + all-chamber forecasts | make weekly |
| Forecast only | make forecast |
| Collect news only | make collect |
| Re-run tagging (LLM if key set) | make tag |
| Data quality report | make qa |
| Model vs GE15 replay | make backtest |
| Enter a poll (sourced, never unsourced) | edit data/seed/polls.csv, then hk load-polls |
| Run tests | make test |
Automatic operation (LIVE): the platform updates itself automatically every night against its production database — during quiet periods the inputs barely change, and the same fixed pipeline runs through campaigns with the cadence they need (collect news → tag → refresh electoral data → quality gates → forecast). Quality-gate blockers stop the update rather than publish on bad data, and an off-site backup is taken every night. Nothing automatic depends on any laptop being awake.
Missed nights. The update clock runs in the cloud — it does not depend on any laptop being awake, and a missed night loses nothing: the site keeps serving the last good forecast (every run is immutable), feeds are re-pulled in full on the next update, and the news layer looks back weeks. If the numbers ever look stale, refresh the page first; if a data problem is suspected, make qa (see the development guide) reports data quality directly.
How the site updates itself (trigger map)
| Step | What triggers it | What happens |
|---|---|---|
| Cycle start | The clock: an automatic nightly update (more often by manual dispatch when events demand it) | Runs the fixed pipeline below against the production database — nothing else ever touches it |
| News in | Cycle step 1 | RSS pulls (static feeds + auto-generated campaign feeds for states within 90 days of polling); articles stored immutable, deduplicated by URL; dead feeds are skipped, never fatal |
| Tagging | Cycle step 2 (only untagged articles) | LLM extracts seats/parties/coalitions + sentiment if LLM_API_KEY is set; deterministic keyword tagger otherwise; results link articles to seat codes and score bloc sentiment |
| Embeddings | Cycle step 3 | pgvector vectors for semantic dedupe (skipped without a key) |
| Notional refresh | Cycle step 4 | Re-aggregates all loaded elections onto today's blocs — coalition-map edits take effect on the next cycle |
| QA gates | Cycle step 5 | Any blocker aborts before forecasting; the site keeps serving the last good run |
| Forecast | Cycle step 6 | Every loaded chamber re-forecast (baseline → swings → polls → news nudges → simulation + formations) and appended as an immutable, input-hashed run |
| Backup | Cycle step 7 | The workflow takes an off-site backup of the database automatically |
| Web update | Every page view | The site renders straight from the live database; after an update the next visitor sees the new numbers, and /movement gains its point automatically |
| Alliance watch | Daily data-entry watch, 02:30 MYT | hk unknown-terms flags trending proper-noun phrases no registry knows — a repeated unknown coalition name is the cue to curate it via the reviewed recipe; the watch never edits data |
| Election menu links | Any page view | The main menu shows links for announced elections (future-dated elections rows — by-elections, PRNs, PRUs) and for state PRNs within ~4 months of their due date; announced elections get the amber pill, due-date outlooks the green one |
Not automatic (deliberately): poll entry (hk load-polls — never unsourced), candidate ingestion at nomination (hk ingest-candidates), new election/results loaders, and any code or config change. These are human/assistant-triggered so every input is a reviewed decision. One partial exception: the daily watch hunts polls automatically (web search for pollster releases) but only reports candidates for entry — a poll enters exclusively through the curated CSV with a source URL. Current poll status: no enterable poll exists for any live chamber yet; Ilham Centre's Melaka fieldwork began mid-October 2026 and the watch will flag the release the day it publishes numbers.
Smarter news analysis: set LLM_API_KEY in .env (any OpenAI-compatible endpoint). The LLM then performs entity extraction and per-bloc sentiment scoring, with embeddings for semantic dedupe; without a key everything still runs on the deterministic keyword tagger.
9. The data behind it
Electoral results come from open datasets (TindakMalaysia for GE15, Wikipedia for GE14 and by-elections) verified against official counts; coalition history is human-curated in-repo. Full provenance and known caveats: data sources.
10. FAQ
How often does the forecast change? Automatically on every cycle — daily in campaign mode, weekly otherwise. Manually after any reviewed data edit (poll, candidates, coalition change) plus make forecast.
Is the site available in Bahasa Malaysia? Yes — toggle EN | BM in the header or visit any page under /ms. The choice is remembered, both languages are indexed by search engines, and the core documents (guide, methodology, data sources, changelog) are translated in full.
Why does a small swing move so many seats? Malaysian margins are thin and FPTP amplifies: dozens of seats sit within a few points, so a 2–3 point national movement routinely shifts 10+ seats.
Why is GPS/GRS forecast almost entirely from history? National news barely observes Sabah/Sarawak interior politics, so the semantic layer contributes little there — the baseline (which is mostly their own recent results) does the work. That is arguably correct.
Can I get the raw numbers? Every figure on the site comes from immutable, input-hashed forecast runs — any figure can be reproduced, and the methodology plus data sources documents trace exactly where each number comes from.
Something looks wrong. Check make qa first, then the seat page's underlying history. Data fixes happen in the seed files and re-seed — never by editing forecast outputs.
Update triggers for this document
Site pages added or changed meaningfully; CLI/make commands changed; configuration knobs added; FAQ entries added; interpretation caveats changed (e.g. after regional-shock modelling lands).