The objective of this analytical overview of Singapore Parliamentary Elections aims to provide a factual analysis of Singapore electoral history through quantitative data, extracted from Elections Department to present an objective assessment of electoral trends. As the nation approaches its next General Election, the motivation of this report seeks to provide an understanding of how demographic changes, past systemic and structural political reforms and recent political developments have shaped Singapore’s parliamentary democracy over time, characterized by the People’s Action Party continued electoral success and the gradual emergence of opposition representation.
The report will reveal several significant findings about Singapore’s evolving electoral landscape. First, the data will show the expansion in registered voter base. The steady increase reflects propulation growth, which is consistent with a young nation, and the increase has implication for electoral boundary revision which in turn will affect strategies of each political parties keen to contest in the next election.
Another observed key trend was the significant fluctuation of political parties with notable peaks and troughs over pre and post independence Singapore. This display suggest notable patterns of consolidation and diversification over the years.
Historically, PAP has been the dominant political party and has maintained parliamentary majority since 1959. The most remarkable feat of PAP is perhaps its clean sweep of parliamentary seats in three consecutive elections, leading to systemic reform. These reforms and other critical findings will be presented in later sections of this report.
With General election due to be held by 23 November 2025, election season is well and truly underway as Singaporeans gear up for political hustings and wait with bated breath for Polling Day to cast their votes. Will the Opposition parties, led by Workers’ Party and the Progress Singapore Party (PSP) mount a serious challenge to the incumbent People’s Action Party (PAP) to gain more political ground? Or will PAP, led by new Prime Minister Mr. Lawerence Wong, and the fourth generation (4G) leaders, contesting their first election since Mr Lee Hsien Loong stepped down as Prime Minister, maintain a strong mandate from the electorates?
This report will look at some interesting data from past to present and try to analyse how various political reforms and developments over the years have come to shape Singapore’s current political landscape.
For this report, two datasets were extracted from the Elections Department (ELD) provided via data.gov.sg API. The datasets are “Parliamentary General Election Results by Candidate” and “Parliamentary General Election - Registered Electors Rejected Votes and Spoilt Ballots”.
“Parliamentary General Election Results by Candidate” dataset consist of 8 variables and 1539 observations.
“Parliamentary General Election Results by Candidate” and “Parliamentary General Election - Registered Electors Rejected Votes and Spoilt Ballots” consist of 6 variables and 721 observations.
One GeoJSON file “ElectoralBoundary2020GEOJSON” was also used to merge with one of the above dataset to plot a choropleth map. Details of data wrangling work, as well as the RMarkdown file embeded, can be found in the Data appendix.The above shows a Time series plot of registered voters growth. There has been a steady increase in registered electors from slightly above three hundred thousands in 1955 to about 2.65 millions in 2020. The growth rate has been relatively consistent, reflecting Singapore’s population growth and increasing voter base.
Following the release of the latest voters roll by the Election Department, the number of eligible voters at the upcoming General Election has increase to 2,715,187.(Koh, 2024). This represents a nine-fold expansion of voter base over a seven decade period. Increase in the number of eligible voters, together with establishment of new towns, would have an impact on the electoral boundaries, which are yet to be determined at the time of writing this report.The time series plot illustrates the number of political parties in Singapore over time, spanning from around 1955 to 2020.From 1955 to late 50s, Singapore witnessed the number of political parties increase from 7 to 11, after which there was a sharp decline to 3 parties in the mid to late 60s.That was the lowest point in the history of Singapore’s political formation. Such period may indicate political consolidation.
Towards the early 1970s, the number of political parties started rising again, reaching a peak of 8 in 1980, and following the momentum, reaching a high of 10 parties in the mid-80s.This could suggest a renewed interest in multi-party democracy during the period of nation building.
After that golden period of growth in number of political parties, there was a general slump in the next 15 years or so until the early 2000s. Hitting a low of 4 parties in mid-2000s.This could be due to political dominance by a few parties which lead to a high barrier to political entry and further eroded political participation.
From 2010 onwards and for the next 10 years, there was a strong surge in the number of political parties in Singapore, hitting an all-time high of 12 parties. This rising trend could very well be brought about by Worker’s Party (WP) historic victory in Aljunied GRC in the 2011 General Election (yahoo! news, 2011). This event marked the first GRC loss for the incumbant People’s Action Party (PAP) and the first time an Opposition party has won a GRC in Singapore’s political history. In the same GE, WP also retained the Hougang SMC seat.
Looking at the trend in this stacked bar chart which depicts the number of seats won by various political parties in the Singapore General election from 1955 to 2020. Except for the 1955 GE where parliamentary seats were shared across various parties, the PAP has been the dominant party which won majority of the parliamentary seats from 1959 right up till 2020 GE.
From the 1970s to 1980, the PAP even made a clean sweep of all the parliamentary seats. PAP won all 65 seats in the 1972 General Election (Sunday Times Reporters, 1972). In the next Election in 1976, it was 69 seats to nothing (Fong, 1976). In the 1980 Election, it was another knockout victory with PAP winning all 75 seats (Fong, 1980). This phenomenal show of mandate to PAP was likely due to their huge support base and the years of consistent governance and steady economic growth. As a results following clean sweep of parliamentary seats by one party in three consecutive Parliamentary General Election, “The Non-Constituency Member of Parliament (NCMP) scheme was introduced just before the 1984 parliamentary general election to ensure that there would always be a minimum number of opposition members in parliament.”(National Library Board Singapore, 2011).
From 1990 onwards, we can see from the chart there were more opposition representation from smaller parties like SDP, SPP and WP.From 2010, only the WP has managed to put in a consistent showing at the polls, retaining and even growing the number of seats in recent Elections.
The above chart illustrates the Top 10 Constituencies by average voter count. The average number of voters ranged from 40000 to 60000 and is heavily influenced by population density in each constituency. These top 10 constituencies are all Group Representation Constituencies (GRC), ranging from 4-members to 6-members, thus they have larger voter base. According to Election Department, “A GRC is a larger electoral division, both in terms of population as well as physical area. A group of MPs represents the interests of those residents in the electoral division.” (ELD Singapore, 2024).
Constitunencies like Pasir Ris-Punggol, Hong Kah and Sengkang, are regions with large residential estates, leading to a larger number of registered voters. Converserly, Holland-Bukit Timah and Marine Parade are areas with more private housing, hence the population densities are lower in those areas.
This stacked bar plot shows the number of constituencies won by each party from 1955 to 2020 General Election. At first glance, one key trend that emerged was the absolute dominance of the ruling party, PAP. From 1955 to 1984 election, the number of constituency has been increasing, oweing to increasing population and registered voters, which led to the building and establishment of more new estates and new towns. At its highest, the number of constituency was close to 80.
After the Group Representation Constituency (GRC) system came into effect in 1988, it was evidence there was a significant decrease of constituencies. Prior to 1988, there was only one type of electoral division, which is Single Member Constituencies (SMC). A GRC covers a larger geographical area, hence the number of individual constituency was reduced quite substanially.
This is a constituency-level analysis to show the Top 10 constituency by rejected votes in the last election in 2020. Ang Mo Kio had the highest number of rejected votes (5,016) Tampines and Pasir Ris-Punggol followed with 3,521 and 3,395 rejected votes respectively The number of rejected votes appears to correlate with constituency size, but if examine from another angle, the constituencies ranked higher in the chart appeared to be from older estates which translate to higher proportion of aging voters.
Shown in the above electoral map is the vote percentage distribution of winning party by electoral boundaries for GE2020 with dark blue colour as low percentage of votes and yellow colour as high percentage of votes. Across all constituencies, there was generally a significant variations in voting pattern. This suggest a more localised voting pattern rather than a uniformed national voting behaviour.
The only GRC in yellow colour is in the West which is Jurong GRC, it has scored very high winning percentage and Jurong has tradiationally been a stronghold of the PAP. Next GRC which is quite close to being yellow colour is Ang Mo Kio GRC, which was helmed by former Prime Minister Mr Lee in GE2020. Ang Mo Kio GRC has very strong support for the incumbent PAP.These two GRCs are considered “safe seats” for the PAP.
Conversely, West Coast GRC in the West and East Coast GRC in the East (both in dark blue colour) scored quite lowly on the vote percentage scales, approximately 50-55%. This shows the winning parties barely secured majority votes and suggest that there were fierce competition from Opposition parties.
Most constituencies in the central region, displayed by pinkish orange colour, showed moderate to strong support.
Such a visualization allow all political parties to identify where the strongholds are, and where the weak spots are. It allows the incumbents to devised strategies, channel extra resources to the weaker area to beef up local support. At the same time, it can serve to inform the Oppositions where potential opportunities lies in future elections.
This line chart shows the trend of Rejection rates (red) and spoilt ballot rates (yellow) over the years. The number of spoilt ballots has remained consistently low relative to the volatile patterns of rejected votes. There are periodic spikes in rejected votes during certain election years. Looking at the specific spike years (1984, 1988, 2011, 2015, and 2020), we can observe that:
Several potential reasons for the periodic spikes in rejected votes include:
This analysis of Singapore’s electoral history reveals an interplay of historical trends and contemoprary developments of the political landscape, underscored by the enduring dominance of PAP alongside incremental gains by the Opposition. These trends indicated a dynamic rather than a static political environment. It also suggest a maturing democracy with young voters yearning for more alternative voices to represent them in Parliament.
Systemic reforms like the NCMP Scheme to have more Opposition voices in Parliament, and structural reform such as the implementation of GRC to get more minorities representation in Parliament, whether they are deemed to be inclusive or created an unfair playing field, have fundamentally shaped Singapore’s parliamentary system.
While past trends points to the PAP maintaining its political dominance in Parliament, they could also suggest a political landscape poised for more competitive elections as more credible Opposition parties try to establish lasting footholds in Singapore’s Parliamentary system.
Elections Department. (2016). Parliamentary General Election - Registered Electors, Rejected Votes and Spoilt Ballots (2024) [Dataset]. data.gov.sg. Retrieved February 23, 2025 from https://data.gov.sg/datasets/d_fdfb854fcb7428b29734d2e0c0674220/view
Koh, F. (2024, August 25). CNA Explains: What could more voters mean for number of MPs, electoral boundaries in Singapore’s next GE? Channel News Asia. https://www.channelnewsasia.com/singapore/cna-explains-what-could-more-voters-mean-number-mps-electoral-boundaries-singapores-next-general-election-4559726
yahoo!news. (2011) WP wins Aljunied GRC, makes key breakthrough. https://sg.news.yahoo.com/wp-wins-aljunied-grc--reports.html?guccounter=1&guce_referrer=aHR0cHM6Ly93d3cuZ29vZ2xlLmNvbS8&guce_referrer_sig=AQAAAJFcZSWwAGQLO1cebtiwYBYDjYMBTmxPwf63uV1lrDilOtMujzBhO8vtX-W4DUIFqKZwM_HHTMN2Lh1MtFJ3wAQG4xoVZzDprkAeF0zbpS-DTHrgEL7yOz4k6ckGkhQBLN4B73EN_nv573QZCDmcBoBv3qX4E7hgxwZ1obskQgwB
National Library Board Singapore (2011). Non-Constituency Member of Parliament scheme is introduced. https://www.nlb.gov.sg/main/article-detail?cmsuuid=64dfccf4-cb6d-441f-aab8-eea923e4d746
Election Department Singapore (2024) TYPES OF ELECTORAL DIVISIONS. https://www.eld.gov.sg/elections_type_electoral.html
Sunday Times Reporters. (1972, September 3) Clean Sweep for the PAP. The Straits Times https://eresources.nlb.gov.sg/newspapers/digitised/article/straitstimes19720903-1.2.2
Fong, L. (1976, December 24). 69 to Nothing. The Straits Times, p1. https://eresources.nlb.gov.sg/newspapers/digitised/article/straitstimes19761224-1.2.2
Fong, L. et al(1980, December 24). 75-0 IT’S ANOTHER CLEAN SWEEP. The Straits Time, p1. https://eresources.nlb.gov.sg/newspapers/digitised/article/straitstimes19801224-1.2.2
For this report, the elections data sets used were from Elections Department (ELD) extracted via the data.gov.sg API. Here I have shown an example of an API to extract data on “Parliamentary GE Results by candidate” from the url https://data.gov.sg/datasets/d_581a30bee57fa7d8383d6bc94739ad00/view
There are 3 main components in the above web API:
1. Base url: https://data.gov.sg/
2. Resource path: api/action/datastore_search
3. Query: ?resource_id=d_581a30bee57fa7d8383d6bc94739ad00
#API to extract data on “Parliamentary GE Results by candidate”
dataset_id <- “d_581a30bee57fa7d8383d6bc94739ad00” elections.url <- paste0(“https://data.gov.sg/api/action/datastore_search?resource_id=”, dataset_id, “&limit=10000”)
out.elections <- fromJSON(elections.url, simplifyDataFrame = T) # Fetching the data df.elections <- out.electionsrecords # Saving the records
In the “Parliamentary GE Results by candidate” dataframe, it contains 8 variables and 1539 rows. The vote_count and vote_percentage variables were in “chr”.
## 'data.frame': 1609 obs. of 8 variables:
## $ _id : int 1 2 3 4 5 6 7 8 9 10 ...
## $ year : chr "1955" "1955" "1955" "1955" ...
## $ constituency : chr "Bukit Panjang" "Bukit Panjang" "Bukit Timah" "Bukit Timah" ...
## $ constituency_type: chr "na" "na" "na" "na" ...
## $ candidates : chr "Goh Tong Liang" "Lim Wee Toh" "S. F. Ho" "Lim Ching Siong" ...
## $ party : chr "PP" "SLF" "PP" "PAP" ...
## $ vote_count : chr "3097" "1192" "722" "3259" ...
## $ vote_percentage : chr "0.7221" "0.2779" "0.1162" "0.5245" ...
The two columns were convert to numeric data type and all “na” were replaced with NA.
#convert columns to numeric
df.elections <- df.elections %>%
mutate( vote_count = as.numeric(vote_count),
vote_percentage = as.numeric(vote_percentage), year = as.numeric(year))
# Replace 'na' with NA for proper handling
df.elections[df.elections == "na"] <- NA
str(df.elections)
## 'data.frame': 1609 obs. of 8 variables:
## $ _id : int 1 2 3 4 5 6 7 8 9 10 ...
## $ year : num 1955 1955 1955 1955 1955 ...
## $ constituency : chr "Bukit Panjang" "Bukit Panjang" "Bukit Timah" "Bukit Timah" ...
## $ constituency_type: chr NA NA NA NA ...
## $ candidates : chr "Goh Tong Liang" "Lim Wee Toh" "S. F. Ho" "Lim Ching Siong" ...
## $ party : chr "PP" "SLF" "PP" "PAP" ...
## $ vote_count : num 3097 1192 722 3259 924 ...
## $ vote_percentage : num 0.722 0.278 0.116 0.524 0.149 ...
The “Parliamentary General Election - Registered Electors Rejected Votes and Spoilt Ballots” dataframe consist of 6 variables and 721 rows. On first inspection, several of the columns were not in the correct data types.
## 'data.frame': 753 obs. of 6 variables:
## $ _id : int 1 2 3 4 5 6 7 8 9 10 ...
## $ year : chr "1955" "1955" "1955" "1955" ...
## $ constituency : chr "Bukit Panjang" "Bukit Timah" "Cairnhill" "Changi" ...
## $ no_of_registered_electors : chr "8012" "9173" "13528" "11239" ...
## $ no_of_rejected_votes : chr "66" "59" "65" "70" ...
## $ no_of_spoilt_ballot_papers: chr "7" "8" "12" "6" ...
Those columns were converted to numeric.
# Convert character columns to numeric
df.regelectors <- df.regelectors %>%
mutate(
no_of_registered_electors = as.numeric(no_of_registered_electors),
no_of_rejected_votes = as.numeric(no_of_rejected_votes),
no_of_spoilt_ballot_papers = as.numeric(no_of_spoilt_ballot_papers),
year = as.integer(year)
)
str(df.regelectors)
## 'data.frame': 753 obs. of 6 variables:
## $ _id : int 1 2 3 4 5 6 7 8 9 10 ...
## $ year : int 1955 1955 1955 1955 1955 1955 1955 1955 1955 1955 ...
## $ constituency : chr "Bukit Panjang" "Bukit Timah" "Cairnhill" "Changi" ...
## $ no_of_registered_electors : num 8012 9173 13528 11239 12242 ...
## $ no_of_rejected_votes : num 66 59 65 70 93 88 61 144 120 60 ...
## $ no_of_spoilt_ballot_papers: num 7 8 12 6 14 16 7 4 14 6 ...
From the “Registered Electors Rejected Votes and Spoilt Ballots” data frame, a summary statistic was created by aggregating each of 3 columns “no_of_registered_electors”, “no_of_rejected_votes” & “no_of_spoilt_ballot_papers” to find each of its total by year and assign it as a new dataframe “yearly_summary”.
# Create yearly summary
yearly_summary <- df.regelectors %>%
group_by(year) %>%
summarise(
total_registered = sum(no_of_registered_electors, na.rm=TRUE),
total_rejected = sum(no_of_rejected_votes, na.rm=TRUE),
total_spoilt = sum(no_of_spoilt_ballot_papers, na.rm=TRUE)
The result is a new dataframe yearly_summary to prepare for visualization
## tibble [17 × 6] (S3: tbl_df/tbl/data.frame)
## $ year : int [1:17] 1955 1959 1963 1968 1972 1976 1980 1984 1988 1991 ...
## $ total_registered: num [1:17] 300299 587780 617650 759367 908382 ...
## $ total_rejected : num [1:17] 1830 6650 5893 2058 15229 ...
## $ total_spoilt : num [1:17] 215 310 204 32 342 308 200 355 580 204 ...
## $ rejection_rate : num [1:17] 0.609 1.131 0.954 0.271 1.676 ...
## $ spoilt_rate : num [1:17] 0.0716 0.05274 0.03303 0.00421 0.03765 ...
To create plot for Rejection rates & spoilt ballots rates, 2 new variables were created in the yearly_summary data frame. Rejection rate, which is total_rejected divided by total_registered. spoilt_rate, which is total_spoilt divided by total_registered. The new features provide more meaningful insights than raw counts from the original data set.
yearly_summary <- yearly_summary %>%
mutate(
rejection_rate = (total_rejected / total_registered) * 100,
spoilt_rate = (total_spoilt / total_registered) * 100
)
# rename Name column to constituency
geo_data <- geo_data %>%
rename(constituency = Name)
# Filter to year 2020 & convert constituency name to Upper case
df.elections2020 <- df.elections %>%
filter(year == 2020) %>%
mutate(constituency = trimws(toupper(constituency)))
# Extract the winning party per constituency
winning_parties <- df.elections2020 %>%
group_by(constituency) %>%
filter(vote_percentage == max(vote_percentage, na.rm=TRUE)) %>%
ungroup()
# Use a left-join to merge df.elections & geo_data by common column "constituency"
combined_data <- geo_data %>%
# left_join(df.elections2020, by = "constituency")
left_join(winning_parties, by = "constituency")
After read in the 2020GeoJSON file and assigned as geo_data, a sanity check found that a column “Name” of geo_data, which shows constituency names are in CAPITAL letters. On the other hand, the df.elections consist of a column “constituency” with the constituency names but not in CAPITAL letters.
Hence I renamed “Name” column in geo_data dataframe to “constituency”. In the df.elections dataframe, I filter to the year 2020 and converted “constituency” to upper case and assigned the dataframe as df.elections2020.
As I wanted to show winning party vote percentage in year 2020, I created a new data frame “winning_parties”, which was grouped by constituency and filtered by the highest vote percentage using max(). I used “na.rm = TRUE” to skip the rows with missing value when performing the max() function so that it would not return a NA should it encounter a missing value.
After that, I merged geo_data with df.elections with a left_join() and by the common column “constituency” and named the new dataframe as combined_data. The new dataframe is ready for plotting.
I wanted to show the names of four GRC on the choropleth map, hence I googled for their respective logitudes and latitudes, assigned their names and constructed a data frame for the places to be marked, as shown in below codes.
#save longitude,latitude & location name
Jurong <- c(103.7216, 1.3346, 'Jurong GRC')
AMK <- c(103.84834, 1.36944, 'AMK GRC')
East <- c(103.9951, 1.3511, 'East Coast GRC')
West <- c(103.72767, 1.27931, 'West Coast GRC')
# Construct the data frame for the places to be marked
places <- rbind(Jurong, AMK, East, West) %>% as.data.frame()
colnames(places) <- c("long", "lat", "ID") # ID contains the name of the places
places$long <- as.numeric(places$long) #ensure the coordinates are numeric
places$lat <- as.numeric (places$lat)