From raw files to this screen

The data pipeline: Extract, Transform, Load, Check

For every step: the methods we used, how much each one caught, and why. Every count is measured by re-running the cleaning code on the raw files.

1

Extract

Get the data. Everything is public. Raw files are kept unedited, each logged with its link, date and fingerprint.

144 downloads logged

DOSM Domestic Tourism Survey

File download from OpenDOSM

20 spreadsheets

WhyVisitors, spending and where tourists come from, by state.

data.gov.my

File download

7 tables

WhyPopulation, income, poverty, basic amenities, GDP.

Tourism Malaysia

The public dashboard's own data API

7 tables

WhyHotels, rooms and how full they are.

OpenStreetMap

Overpass API, one query per kind of place

22,872 map points

WhyAttractions, airports, rail stations, hotels, eateries.

Travel web pages

Exa search API, 295 searches

2,950 pages

WhyWhat travellers write, in Malay, English and Chinese.

Video comments

YouTube Data API

9,229 comments

WhyShort first-hand reactions to each state.

Wikivoyage and rainfall

MediaWiki API and Open-Meteo API

83 pages, 37 towns

WhyReal places and ten years of rain for the trip planner.

2

Transform

Clean the data. Each row is one method: what it does, how much it caught, and why.

18,420 texts → 7,561 real trips

Official tables

Find rows by their label

Search for the row that says "Johor" instead of trusting cell B12.

3,671 cells read

WhyThe sheets are laid out for printing, so positions shift from file to file.

Dashes stay empty

"-" and "n.a." become an empty cell, never 0.

26 cells

WhyA dash means "not published". A zero would mean "nobody came".

One code per state

Every spelling is mapped to one 3-letter code.

21 → 16 spellings

Why"Penang" and "Pulau Pinang" must be the same state, or tables from different agencies cannot be matched.

Remove repeated state-years

Two DOSM editions cover the same years. One copy is kept.

112 of 256 removed

WhyCounting a state twice in one year would double its visitors.

Keep only the rows we need

Drop the rows split by sex, age group or sub-total; keep the "everyone" row.

1,063 of 276,015 kept

WhyEach state and year must appear exactly once, or the join would multiply rows.

Map points inside a state

Each point is placed in the state whose border it falls inside; repeats are dropped.

129 of 22,872 removed

WhyA point out at sea or listed twice cannot be counted for a state.

Travel posts

Skip review sites

Pages from TripAdvisor, Google Maps, Facebook, Instagram and TikTok are not read.

19 of 2,950 pages

WhyTheir terms do not allow reviews to be copied.

Drop junk paragraphs

Menus, cookie notices, "subscribe", headings and one-liners under 40 characters.

89,146 of 150,175

WhyMost of a web page is not travel writing.

Drop very short comments

Comments under 60 characters.

5,146 of 9,229

Why"Nice video!" says nothing about a place.

Remove personal details

Links 1,394, e-mails 317, usernames 877, phone numbers 832.

3,420 removed

WhyPrivacy. We need what was said, not who said it.

Cap long pages

At most 6 passages are taken from one page.

5,259 passages left out

WhyOne very long blog must not drown out a whole state.

Remove exact duplicates

Same text after ignoring capitals, spaces and punctuation.

1,914 of 18,420

WhyA repeated post would count one opinion many times.

Remove near-duplicates

Texts sharing at least 70% of their 5-word runs with an earlier text.

31 more removed

WhyThe same article copied to a second site with a word changed.

AI filter: is this a real trip?

Gemini 2.5 Flash Lite reads each text and answers from fixed lists.

2,956 not a trip

WhyAdverts, news and "I want to go one day" are not experiences.

Ask a second time

The same yes/no question again, more strictly. Kept only on two yeses.

5,861 more removed

WhyOne pass let too much chatter through. Fewer posts, but cleaner.

Wrong-state check

If the place the AI found names a different state, the post is not counted.

64 removed

WhyA search for Sabah that returns a post about Kuala Lumpur must not score Sabah.

Steady small samples

A topic with few mentions is pulled towards the national average.

176 state-topic scores

WhyTwelve posts should not decide whether a state is called safe.

3

Load

Store the clean tables, join them into one row per state per year, work out the scores, and hand small files to the website.

80 rows × 53 columns

Save as clean tables

Each cleaned source is stored as a typed Parquet table, with a data dictionary.

21 tables, 45,198 rows

WhyFixed column types, so a number can never quietly turn into text.

Match by state code and year

Every clean table is looked up for each of the 16 states. No state is ever dropped.

80 × 53 rows × columns

WhyOne table means every score is worked out from the same numbers.

Carry the latest year forward

If a year is not published, use the most recent earlier year and record which year it was.

272 of 3,840 cells

WhyDOSM's state spending stops at 2023. We say so on screen instead of guessing.

Never fill a gap

A value that was never published stays empty.

98 cells left empty

WhyAn invented number looks as real as a true one. Almost all are 2021 detail DOSM did not publish.

Work out the scores

Gini, Opportunity, Bottleneck Diagnoser, Room to grow, Simulator.

5 scores

WhyAnyone can check by hand why a state ranks where it does. Steps are on the Methodology page.

Ship small files

The final table and scores are exported as JSON with the website. No database.

12 files

WhyNothing to log in to, nothing to go down.

4

Check

Prove nothing went wrong. If any check fails, the pipeline stops and nothing is published.

110 automatic tests

Range rules

Percentages must be 0-100, counts cannot be negative, nights stayed must be believable.

0 problems in 2,426 values

WhyA value outside its range means we read the file wrongly. The pipeline stops if one appears.

Every state, every year

Each year must hold exactly the 16 states, once each.

0 missing of 224

WhyA missing or doubled state would bend every share and every ranking.

Totals must match the publisher

Our 16 states added up = DOSM's national total; tourists + day-trippers = visitors; state rooms = Tourism Malaysia's total.

0 failed of 46 checks

WhyIt proves nothing was lost or double-counted. 2025: 290,064.7 = 290,064.7 thousand visitors.

Compare the two editions

Where both editions give the same state and year, do the numbers agree?

0 of 112 differ

WhyThey agree everywhere, so dropping the repeated copy loses nothing.

Outlier scan

Flag any yearly jump in visitors far from the usual change (robust z-score above 3.5).

14 of 128 flagged

WhyAll are 2022: travel bouncing back after the pandemic. Real, so kept unchanged. A flag in a normal year would point to an error.

Automatic tests

53 on the data and the maths, 57 on the website.

110 must pass

WhyIf one fails, nothing is published.

Two calculators must agree

The website recalculates every score live; its answers are compared with Python's.

5 decimal places

WhySo the screen can never drift from the pipeline.

Shake the weights

Re-rank the states 1,000 times with random weights.

0.94 of 1 agreement

WhyThe ranking must not depend on the weights we happened to choose.

Counted from the pipeline's files on 2026-09-20.