CompareSG

Haze in Singapore by year and part of the island

Pick a part of Singapore to see how many hours of unhealthy air it had in each year since 2014. NEA's 24-hour PSI, one reading an hour for five parts, to 13 Oct 2026, 12pm.

Part of Singapore

Also set by clicking a year or a cell below.
2014 starts on 1 Apr, when the data starts.

An hour counts as unhealthy here when NEA's 24-hour PSI for a part is 101 or higher. NEA calls 101 to 200 unhealthy, 201 to 300 very unhealthy and over 300 hazardous.

How much unhealthy air was there?

At least one part of Singapore, in 2026 so far (to 13 Oct)

447hours at PSI 101 or higher
Place 2 of 13 years27 days with an hour at 101 or higherHighest reading 192 (9 Oct, Central)Longest stretch 204 hours in a row, still going at the last reading

The most in this view was 856 in 2015. The year is not over.

Year by year

Each row is a year, the most first. A longer bar means more. Hover or tap a row for details, and click to look at that year.

201 or higher (very unhealthy)101 to 200 (unhealthy)Year shown above

At least one part of Singapore: 2015 had the most (856 hours). 4 of the 13 years had none.

Which part of Singapore, in 2026 so far (to 13 Oct)?

Five circles, one for each part, sized by the count. They sit near the middle of each part; NEA names five regions but gives no boundaries, so the positions are approximate. Click a circle or a row to pick a part.

North193South219East301West313Central447

Hours at 101 or higher (solid part: 201 or higher)Part picked above

In 2026 so far (to 13 Oct), Central had the most (447 hours), then West (313), East (301), South (219) and North (193).

Every year, every part

The darker the cell, the higher the count. The outlined cell is the part with the most in that year. Click a cell to look at it.

YearAnywhereAnyNorthNSouthSEastEWestWCentralCAll fiveAll 5

Part with the most, by year (outlined above): 2014: West; 2015: West; 2016: North; 2017: North; 2019: South; 2021: North; 2023: East; 2025: North; 2026 so far: Central. Years not listed have none.

2026, day by day

The highest 24-hour PSI on each day from 1 Aug to 13 Oct 2026, for the part you picked. Dark bars are days that reached 101 or higher. The dotted line is 101.

At least one part of Singapore: 27 of the 74 days had a reading of 101 or more. The highest was 192 on 9 Oct.

Fun finds

Does the same part always have the most?
No: 5 different parts have led
West had the most in 2014 and 2015, South in 2019 and Central in 2026 so far. Central had the fewest of the five in 2014, 2015 and 2019. Pick a year to see the order.
What was the longest stretch in a row?
204 hours at 101 or higher in 2026
That is 8.5 days, anywhere in Singapore, and it was still going at the last reading on 13 Oct 2026. The longest stretch in 2015 was 164 hours and in 2019 it was 53.
How high did the PSI get?
192 in Central on 9 Oct
It held at 192 from 5pm to 8pm on 9 Oct 2026, the highest reading of 2026 so far. The highest in the data is 322, in the South on 25 Sep 2015, 8am.

The short version

Central had the most hours at PSI 101+ in 2026 so far (447); West in 2015 (836). By year and part of Singapore since 2014. NEA data, 13 Oct 2026.

What we did. We took NEA's 24-hour PSI for the five parts of Singapore, one reading an hour, from April 2014 to 13 Oct 2026, 12pm (109,884 hours). The history is the data.gov.sg file and the last weeks (from 1 Sep 2026) are NEA's real-time feed. For each year and each part we counted the hours with a PSI of 101 or higher, the days with such an hour, the highest reading and the longest stretch in a row. "Anywhere" is any hour when at least one part was at 101 or higher; "all five" is any hour when every part was.

What we found. In 2026 so far, Central had the most hours at 101 or higher (447), then West (313), East (301), South (219) and North (193). All five parts were at 101 or higher together for 179 hours on 12 days, the first time at 15 Sep 2026, 1am; before that it last happened at 23 Sep 2019, 3pm. The highest reading this year is 192 in Central, from 5pm to 8pm on 9 Oct 2026. In 2015 the same count was 836 hours in the West, 856 hours anywhere, and the highest reading was 322 in the South; in 2019 it was 147 hours in the South and 156 anywhere. The part with the most hours has not been the same each time: North: 2016, 2017, 2021 and 2025; West: 2014 and 2015; South: 2019; East: 2023; Central: 2026 so far.

What to keep in mind. The PSI is a rolling 24-hour reading, so an hour at 101 or more means the last 24 hours were at that level, not that one hour was. 2026 is not over, and haze can run past 13 Oct (in 2015, 269 of the 856 hours came after it); the "same dates" view compares each year only up to 13 Oct. NEA gives five regions, not exact places, so a street can differ from its region. We do not say what caused any haze, and we did not check whether NEA changed how the PSI is worked out over these years.

By Ben (opens in a new tab). Updated . Data: NEA 24-hour PSI on data.gov.sg and NEA's real-time PSI feed, to 13 Oct 2026, 12pm.

Questions people ask

How many hours of unhealthy air has Singapore had each year?

Counting an hour when at least one of NEA's five parts had a 24-hour PSI of 101 or higher: 2014 (from April) 114, 2015 856, 2016 55, 2017 5, 2018 0, 2019 156, 2020 0, 2021 6, 2022 0, 2023 30, 2024 0, 2025 4, 2026 (so far) 447. Data to 13 Oct 2026, 12pm.

Which part of Singapore had the most unhealthy hours in 2026?

Central, with 447 hours at 101 or higher up to 13 Oct 2026. Then West (313), East (301), South (219) and North (193).

Is the haze in 2026 worse than in 2019 or 2015?

By these counts, 2026 so far is higher than 2019 and lower than 2015. Hours anywhere at 101 or higher: 447 in 2026 so far, 156 in 2019, 856 in 2015. All five parts at once: 179, 75 and 596 hours. Highest reading: 192, 154 and 322. Hours at 201 or higher: 0, 0 and 81. 2026 is not over, so its counts can still grow.

Which part had the most unhealthy hours in earlier hazy years?

2015: West (836), South (781), North (741), East (683) and Central (603). 2019: South (147), West (108), East (100), North (79) and Central (78). 2014: West (83), South (76), East (31), North (29) and Central (24).

Is it always the same part of Singapore?

No. The part with the most hours was North in 2016, 2017, 2021 and 2025; West in 2014 and 2015; South in 2019; East in 2023; Central in 2026 so far. Central had the fewest of the five in 2014, 2015 and 2019, and the most in 2026 so far.

When were all five parts unhealthy at the same time?

In 2026 so far for 179 hours on 12 days, first at 15 Sep 2026, 1am and still the case at the last reading on 13 Oct 2026. Before this year the last time was 23 Sep 2019, 3pm. By year: 2014 19, 2015 596, 2019 75, 2026 179 hours.

What was the highest PSI reading?

In this data the highest reading was 322, in the South on 25 Sep 2015, 8am. In 2026 it is 192, in Central, from 5pm to 8pm on 9 Oct 2026. In 2019 it was 154 (South, 19 Sep 2019, 2am).

What is the PSI and what do the numbers mean?

The Pollutant Standards Index is NEA's air quality number. NEA works out a sub-index for each pollutant and takes the highest as the PSI. The 24-hour PSI looks back over the last 24 hours. NEA's bands: 0 to 50 good, 51 to 100 moderate, 101 to 200 unhealthy, 201 to 300 very unhealthy, over 300 hazardous.

What does an hour at 101 or higher mean here?

NEA publishes the 24-hour PSI every hour for each of five regions. We count one hour for each hourly reading of 101 or higher. Because the PSI looks back 24 hours, a stretch of hours at 101 or higher shows how long the 24-hour reading stayed at that level, not how many hours the air itself was bad.

Why is 2026 compared with whole years?

It is not, unless you choose it. The default view shows each year as a whole, and 2026 is only 6,853 hours old. Choose "same dates each year" to compare every year only from 1 Jan to 13 Oct. In 2015, 269 of the 856 unhealthy hours came after 13 Oct.

Does this say what caused the haze or what to do?

No. It counts NEA's readings. For health advice, see NEA's own advisories.

Where does the data come from?

NEA's 24-hour PSI on data.gov.sg: the historical hourly file for April 2014 to 31 Aug 2026, and NEA's real-time PSI feed for 1 Sep 2026 to 13 Oct 2026, 12pm. Details are in the boring bit below.

The numbers behind it

Hours with a 24-hour PSI of 101 or higher, by year and part (each whole year)
YearHours of dataAnywhereNorthSouthEastWestCentralAll fiveAnywhere, 201 or higher
2014 (from 1 Apr)6,5991142976318324190
20158,76085674178168383660359681
20168,7845554130261300
20178,76055000000
20188,76000000000
20198,7601567914710010878750
20208,78400000000
20218,76066000000
20228,76000000000
20238,76030023002600
20248,78400000000
20258,76044000000
2026 so far6,8534471932193013134471790
The same count for the same dates each year, 1 Jan to 13 Oct
YearHours of dataAnywhereNorthSouthEastWestCentralAll fiveAnywhere, 201 or higher
20144,703761856244716120
20156,86458751752647056842041364
20166,8885554130261300
20176,86455000000
20186,86400000000
20196,864144791389610878750
20206,88800000000
20216,86466000000
20226,86400000000
20236,86430023002600
20246,88800000000
20256,86400000000
2026 so far6,8534471932193013134471790
Days with at least one hour at 101 or higher, by year and part (each whole year)
YearAnywhereNorthSouthEastWestCentralAll five
2014 (from 1 Apr)9374722
201546434239453838
20164420220
20171100000
20180000000
2019115118855
20200000000
20212200000
20220000000
20232012020
20240000000
20251100000
2026 so far27121417172712
Highest 24-hour PSI reading by year and part
YearAnywhereNorthSouthEastWestCentralAll fiveWhen the highest reading anywhere was first reached
20141251081241161251131077 Oct 2014, 9am, West
201532230232228532126826825 Sep 2015, 8am, South
2016143143105911431089127 Aug 2016, 7am, North
2017143143968983807315 Mar 2017, 9pm, North
20189175837791846915 Mar 2018, 7pm, West
201915413115414513513113119 Sep 2019, 2am, South
20208484697166766519 Oct 2020, 6pm, North
2021124124667466756328 Mar 2021, 7pm, North
20227571667366756413 Apr 2022, 6am, Central
20231239610112367117677 Oct 2023, 8pm, East
20247770677376776126 Jul 2024, 2pm, Central
2025114114667371776530 Nov 2025, 6pm, North
2026 so far1921361681701871921369 Oct 2026, 5pm, Central
2026 day by day: the highest reading on each of 74 days
Highest 24-hour PSI reading on each day, 1 Aug to 13 Oct 2026
DayAnywhereNorthSouthEastWestCentralAll five
1 Aug 202662565657566255
2 Aug 202661565558596154
3 Aug 202667565659626756
4 Aug 202667565660626756
5 Aug 202662555559596254
6 Aug 202674596267667459
7 Aug 202674586267667458
8 Aug 202671605963617159
9 Aug 202681656471718164
10 Aug 202680646172748061
11 Aug 202680656272748062
12 Aug 202678626269717861
13 Aug 202677636368697763
14 Aug 202677636670677763
15 Aug 202681636869708163
16 Aug 202678616367687861
17 Aug 202673606165667359
18 Aug 202672596065637259
19 Aug 202675616267657559
20 Aug 202676636068657660
21 Aug 202674606364647460
22 Aug 202680626770708062
23 Aug 202682646671718264
24 Aug 202698727587799872
25 Aug 202699717487809971
26 Aug 202689657374738965
27 Aug 202675636366687563
28 Aug 202679636767707962
29 Aug 202684637171728463
30 Aug 202678636768687863
31 Aug 202669586264646958
1 Sep 202676626266687662
2 Sep 202677626568697762
3 Sep 202688697276808869
4 Sep 202612388871049912387
5 Sep 2026123878810410312387
6 Sep 202693757991869373
7 Sep 202698748193909874
8 Sep 202683666975768366
9 Sep 20261027681838710276
10 Sep 20261067780878710677
11 Sep 202699767786859975
12 Sep 202682707280798270
13 Sep 202684697180758469
14 Sep 20261419710612111714197
15 Sep 2026154104111128128154104
16 Sep 20261128380939611280
17 Sep 20261037570819510370
18 Sep 20261027470809310270
19 Sep 20261138482979611382
20 Sep 2026123858910210612385
21 Sep 202693687073909368
22 Sep 202684656974778465
23 Sep 202673626270736762
24 Sep 202684686979847868
25 Sep 202681646372778163
26 Sep 20261158084999711580
27 Sep 2026132929711910613292
28 Sep 2026117791119810711779
29 Sep 2026167109133139150167109
30 Sep 2026138104112128135138104
1 Oct 202696798995969679
2 Oct 202691717780809171
3 Oct 20261057987969410579
4 Oct 20261037983948910378
5 Oct 2026134106104112127134104
6 Oct 2026136111103119125136103
7 Oct 2026150109120132150144109
8 Oct 2026162114124133162148114
9 Oct 2026192136168170187192136
10 Oct 2026183133155162178183133
11 Oct 2026162120123144155162120
12 Oct 2026166125123144166159123
13 Oct 2026 (to 12:00)174129134156174170129

The boring bit

Where the data comes from

NEA's 24-hour PSI for five regions (north, south, east, west and central), one reading an hour. History: the data.gov.sg file of the hourly 24-hour PSI, 108,864 rows from 1 Apr 2014 01:00 to 1 Sep 2026 00:00. From 1 Sep 2026 00:00 we use NEA's real-time PSI feed (api-open.data.gov.sg), fetched on 13 Oct 2026, which runs to 12pm that day. The two overlap for 169 hours (25 Aug to 1 Sep 2026): they agree on every reading except 2 West readings on 26 Aug 2026, one point or two apart (69 and 68, 69 and 67), all far below 101. The PSI bands (good, moderate, unhealthy, very unhealthy, hazardous) are NEA's, from its air pollution FAQ.

What is not claimed

  • We do not say what caused any haze, whether 2026 will end worse or better than another year, or what anyone should do. The year is not over: the data stops at 13 Oct 2026, 12pm.
  • The 24-hour PSI looks back over the last 24 hours. Hours at 101 or higher show how long that reading stayed at that level. They are not a count of hours when the air itself was bad.
  • "Anywhere" (the highest of the five readings at that hour) and "all five" (the lowest of the five) are our own measures. We did not check how NEA works out a national PSI, so we do not call "anywhere" that.
  • NEA gives five regions without boundaries. The circles on the map are placed by eye and mean no more than that.
  • We did not check whether NEA changed how the PSI is worked out, or where it measures, between 2014 and 2026. Years far apart may not compare exactly.
  • The 1-hour PM2.5 readings, other pollutants and the 3-hour PSI are not used.
  • Counts are of readings as the files give them. NEA could revise past readings.
  • Whole years and part years are different lengths: 6,599 hours in 2014 (from 1 Apr) and 6,853 in 2026. The "same dates" view compares 1 Jan to 13 Oct in every year.

Coverage

109,884 hours, from 1 Apr 2014 01:00 to 13 Oct 2026 12pm, with one reading for every hour and no hour missing. Every region has a value in every hour. Four rows in the history file carried the wrong timestamp (one written "2016-04-04 010:00:00", and three hours written twice: 7 Mar 2016 0:00, 24 Apr 2018 0:00, 16 Aug 2018 2:00). The file is in time order, so we gave each one the single hour missing at its place in the file: 4 Apr 2016 00:00, 7 Mar 2016 12:00, 25 Apr 2018 00:00 and 16 Aug 2018 03:00. All four sets of readings are below 70, so no count or highest reading changes whichever way they are read.

Report an error. Corrections are logged publicly with a date.

How we did it
Two sources joinedHourly countsRuns of hours
  1. Read the history file and repaired the four timestamps above.
  2. Read NEA's real-time files, one for each day from 25 Aug to 13 Oct 2026, and compared the overlap.
  3. Joined them: history up to 31 Aug 2026 23:00 and real-time from 1 Sep 2026 00:00, one row for every hour.
  4. Made two more series for each hour: the highest of the five readings ("anywhere") and the lowest ("all five").
  5. Counted for each year, each part and each of the two ways of comparing years: hours at 101 or higher and at 201 or higher, days with such an hour, the highest reading and when it was first reached, and the longest stretch of hours in a row.
Show the Python code (step 1 to 3)
"""combine_psi.py - joins the two NEA 24-hour PSI sources into one hourly file for the haze page.
Run (inside the build folder):  python3 -I prep/combine_psi.py <out.csv>
  reads data/raw/historical_24hr_psi.csv and data/raw/realtime/*.json
1. Historical file (data.gov.sg, NEA 24-hour PSI, one row an hour, 1 Apr 2014 01:00 to 1 Sep 2026 00:00, 108,864 rows).
   Four rows have a timestamp that does not fit their place in the file (see FIXES). Each is set to the one hour that is
   missing between its neighbours. All four readings are below 70, so no count of hours at 101 or higher changes.
2. Real-time files (api-open.data.gov.sg /v2/real-time/api/psi?date=YYYY-MM-DD, one file for each day, fetched 13 Oct 2026).
   Used from 1 Sep 2026 00:00 on. For 25 to 31 Aug, where the two sources overlap, the differences are counted and printed.
Standard library only."""
import csv, glob, json, os, sys
from datetime import datetime, timedelta
HERE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
R = ['north', 'south', 'east', 'west', 'central']
out = sys.argv[1]
# _id of the row -> the hour it belongs to (the file is in time order, so the hour is the one missing at that place)
FIXES = {17616: '2016-04-04 00:00',   # written "2016-04-04 010:00:00"
         16956: '2016-03-07 12:00',   # written 7/3/2016 0:00 twice; 12 rows after the first
         35640: '2018-04-25 00:00',   # written 24/4/2018 0:00 twice; 24 rows after the first
         38355: '2018-08-16 03:00'}   # written 16/8/2018 2:00 twice
FMT = '%Y-%m-%d %H:%M'
hist = {}
rows_n = 0
for row in csv.DictReader(open(os.path.join(HERE, 'data/raw/historical_24hr_psi.csv'), newline='')):
    rows_n += 1
    i = int(row['_id'])
    if i in FIXES:
        ts = datetime.strptime(FIXES[i], FMT)
    else:
        ts = datetime.strptime(row['timestamp'], '%d/%m/%Y %H:%M')
    assert ts not in hist, ('two rows for one hour', ts)
    hist[ts] = [int(row[r]) for r in R]
ts_sorted = sorted(hist)
assert all(b - a == timedelta(hours=1) for a, b in zip(ts_sorted, ts_sorted[1:])), 'history is not one row per hour'
rt = {}
for fn in sorted(glob.glob(os.path.join(HERE, 'data/raw/realtime/*.json'))):
    for it in json.load(open(fn))['data']['items']:
        p = it['readings']['psi_twenty_four_hourly']
        rt[datetime.strptime(it['timestamp'][:16].replace('T', ' '), FMT)] = [p[r] for r in R]
overlap = [t for t in rt if t in hist]
diff = {r: sum(1 for t in overlap if hist[t][k] != rt[t][k]) for k, r in enumerate(R)}
print('overlap rows', len(overlap), 'differences', diff)
cut = datetime(2026, 9, 1, 0, 0)
rows = [(t, hist[t], 'historical') for t in ts_sorted if t < cut] + [(t, rt[t], 'realtime') for t in sorted(rt) if t >= cut]
assert all(b[0] - a[0] == timedelta(hours=1) for a, b in zip(rows, rows[1:])), 'a gap between the two sources'
with open(out, 'w', newline='') as f:
    f.write('ts,' + ','.join(R) + ',src\n')
    for t, v, s in rows:
        f.write(t.strftime(FMT) + ':00,' + ','.join(str(x) for x in v) + ',' + s + '\n')
print('rows', len(rows), rows[0][0], rows[-1][0], 'rows from the history', sum(1 for r in rows if r[2] == 'historical'), 'rows from the real-time feed', sum(1 for r in rows if r[2] == 'realtime'))
Show the Python code (step 4 and 5)
"""prep_haze.py - builds data/data.json for the CompareSG haze page.
Run (inside the build folder):  python3 -I prep/prep_haze.py <hourly.csv> <out.json>
Input : the hourly file made by prep/combine_psi.py (NEA 24-hour PSI, one row an hour, five regions: the data.gov.sg
        historical file to 31 Aug 2026 23:00, then NEA's real-time feed).
A 'view' is one series with one value an hour: a region, 'any' (the highest of the five readings that hour) or
'all5' (the lowest of the five, so 101 or more means every part was at 101 or more).
For each year, window (the whole year, or 1 Jan to the last day's date) and view it counts the hours at 101 or higher
(and at 201 or higher), the days with such an hour, the highest reading (and the first hour it was reached) and the longest
unbroken run of hours at 101 or higher. Standard library only."""
import csv, json, sys
from datetime import datetime
src, out = sys.argv[1], sys.argv[2]
R = ['north', 'south', 'east', 'west', 'central']
VIEWS = ['any'] + R + ['all5']
rows = []
for r in csv.DictReader(open(src, newline='')):
    t = datetime.strptime(r['ts'], '%Y-%m-%d %H:%M:%S')
    v = {k: int(r[k]) for k in R}
    v['any'] = max(v[k] for k in R)
    v['all5'] = min(v[k] for k in R)
    rows.append({'ts': t, 'year': t.year, 'date': t.strftime('%Y-%m-%d'), 'md': t.month * 100 + t.day, **v})
last = rows[-1]['ts']
assert all(a['ts'] < b['ts'] for a, b in zip(rows, rows[1:]))
CUT = last.month * 100 + last.day
stamp = lambda t: t.strftime('%Y-%m-%d %H:%M')

def one(g, v):
    hi = [x[v] >= 101 for x in g]
    pk = max(x[v] for x in g)
    first = next(x for x in g if x[v] == pk)
    o = {'h': sum(hi), 'v': sum(1 for x in g if x[v] >= 201), 'd': len({x['date'] for x in g if x[v] >= 101}),
         'p': pk, 't': stamp(first['ts'])}
    if v == 'any':
        o['pr'] = [r for r in R if first[r] == pk][0]
    longest = cur = 0
    for f in hi:
        cur = cur + 1 if f else 0
        longest = max(longest, cur)
    o['r'] = longest
    o['open'] = bool(cur > 0 and cur == longest and g[-1]['ts'] == last)
    return o

res = {'last': stamp(last), 'cut': '%02d-%02d' % (CUT // 100, CUT % 100), 'views': VIEWS, 'Y': {}}
for y in sorted({x['year'] for x in rows}):
    g = [x for x in rows if x['year'] == y]
    res['Y'][y] = {'hrs': len(g), 'first': stamp(g[0]['ts']), 'full': {v: one(g, v) for v in VIEWS}}
    gw = [x for x in g if x['md'] <= CUT]
    res['Y'][y]['same'] = {v: one(gw, v) for v in VIEWS}
    res['Y'][y]['hrs_same'] = len(gw)

# the current year day by day, 1 Aug to the last day: the highest reading each day in every view
cal = []
dates = sorted({x['date'] for x in rows if x['year'] == last.year and x['ts'] >= datetime(last.year, 8, 1)})
for dt in dates:
    gg = [x for x in rows if x['date'] == dt]
    cal.append({'d': dt, 'n': len(gg), **{v: max(x[v] for x in gg) for v in VIEWS}})
res['cal'] = cal

# facts for the sentences
cur_y = last.year
a = [x for x in rows if x['year'] == cur_y and x['all5'] >= 101]
b = [x for x in rows if x['year'] < cur_y and x['all5'] >= 101]
res['all5_first_2026'] = stamp(a[0]['ts'])
res['all5_prev_last'] = stamp(b[-1]['ts'])
c = [x for x in rows if x['year'] == cur_y and x['central'] == 192]
res['c192'] = [stamp(c[0]['ts']), stamp(c[-1]['ts']), len(c)]
res['gaps'] = sum(1 for x, y2 in zip(rows, rows[1:]) if (y2['ts'] - x['ts']).total_seconds() != 3600)
json.dump(res, open(out, 'w'), separators=(',', ':'))
print('ok', last, 'gaps in hourly steps:', res['gaps'])

What could make it wrong: a past reading NEA has since changed, a timestamp error in the files that we did not catch, or a stretch where a station fed no data. As a check, the hour counts by year add up to the 109,884 hours in the joined file, and a second count written separately gives the same figures.

No machine learning was needed. The question was "how many hours, and where", which counting answers directly.

Sources: NEA 24-hour PSI on data.gov.sg (opens in a new tab), NEA real-time PSI feed (opens in a new tab) and NEA air pollution FAQ (opens in a new tab), read 13 Oct 2026. Contains information from the NEA 24-hour PSI data sets (the historical file and the real-time feed), accessed on 13 Oct 2026 from NEA via data.gov.sg, made available under the Singapore Open Data Licence version 1.0 (opens in a new tab).