Visualizing Language Loss in Taiwan: Create an “Age-Sex Pyramid of Language” with ggplot2

[This article was first published on Yongfu's Blog, and kindly contributed to R-bloggers]. (You can report issue about the content on this page here)
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.

Taiwan Language Survey is a small project I worked on during May to June in 2018. The idea was to create a survey that continuously collects data and a web page that visualizes the collected data. The web page is updated weekly using Travis-CI.

The main purpose of this survey is to raise public awareness of language loss in Taiwan. Hence, the survey is designed to collect data that can provide valuable information about language loss, for example, some questions were asked to gain insight about the change of linguistic competence acoss generations in a family (i.e. across the subject’s parents, the subject, and the subject’s children). In addition to changes within family, information about the age of the subjects is also colleceted, meaning that we can see how linguistic competence changes among diffrent age groups of subjects in a community, i.e. is a language becoming more dominant or entering the dying process?

Visualization is a powerful tool to capture how linguistic competences of different langauges are changing. But creating visualizations necessitates creativity – how can language loss be visualized? Below, I illustrate one of the methods I created for visualizing language loss – a visaulization inspired by the age-sex pyramid.

Age-Sex Pyramid of Language

The age-sex pyramid is used to visualize the population structure of a community. The vertical axis indicates the age and each horizontal bar represents an age group. The horizontal axis indicates the population size of male or female of a particular age group.

The age-sex pyramid is a great tool to visualize the population structure since the ‘shape’ of the pyramid gives readers a lot information. For example, an ‘expansive pyramid’ has longer bars at the bottom of the pyramid, which indicates the population is young and growing. A ‘stationary pyramid’ looks like a rectangular bar, indicating the population sizes of different age groups are about the same. A ‘constructive’ pyramid indicates a shrinking population, which is narrowed at the bottom.

Similarly, a modified version of the age-sex pyramid, which I’ll call the ‘age-sex pyramid of language’, can be used to visualize the population structure of a language and predicts the language’s vitality. Instead of visualizing population size, the age-sex pyramid of language visualizes the average fluency of a language on the horizontal axis.

An age-sex pyramid of Taiwanese. The red bars on the left indicates females of different age groups and the blue bars on the right indicates males. The average fluency (values on the horizontal axis) is calculated from a six-point scale (0-5) on Taiwanese fluency.

Figure 1: An age-sex pyramid of Taiwanese. The red bars on the left indicates females of different age groups and the blue bars on the right indicates males. The average fluency (values on the horizontal axis) is calculated from a six-point scale (0-5) on Taiwanese fluency.

As shown in Figure 1, the shape of the age-sex pyramid of Taiwanese in Taiwan1 is an ‘inverted triangle’, which is almost never seen in the conventional population pyramid. However, this inverted triangular shape is expected to appear quite often, since it indicates an ongoing language loss in a community.

Vitality of Language Shape of Pyramid
Shrinking and Dying Inverted Triangle
Growing Triangle
Stable Retangualr Bar

Drawing Age-Sex Pyramid with ggplot2

As complex as it might seem, an age-sex pyramid created with ggplot2 is actually a (modified) bar chart. I learned this on stackoverflow, and the trick is

  1. Use ifelse to flip the value (here, population size) according to the gender of the age group
  2. Use geom_bar(stat = "identity") to let the heights of the bars represent values in the data frame, i.e. the value given to y
  3. Use coord_flip() to make the bars horizontal
df0 <- tibble::tibble(
  Age = rep(c('10-19', '20-29', '30-39'), 2),
  Gender = rep(c('Female', 'Male'), each = 3),
  PopSize = sample(0:100, size = 6, replace = T)
Age Gender PopSize
10-19 Female 26
20-29 Female 37
30-39 Female 57
10-19 Male 91
20-29 Male 20
30-39 Male 90
pl <- ggplot(df0, aes(x = Age, 
                      y = ifelse(Gender == 'Male', PopSize, -PopSize),
                      fill = Gender)) +
  geom_bar(stat = 'identity') +

To center the plot (i.e. to make the point where population size is zero at the center of the plot), we have to scale the axis of ‘population size’ (y) with scale_y_continuous:

pl + scale_y_continuous(limits = c(-100, 100),
                        breaks = seq(-100, 100, 25),
                        labels = abs) +
  labs(y = 'Population Size')

where abs in labels is the function abs(). By default, ggplot2 pass the value given in breaks to the the function specified in labels.

Visualizing Language Loss

To create an age-sex pyramid of language, the data structure needed is exactly the same as the one above, except the variable, PopSize, is replaced by ‘average fluency’ of a language. But since most people in Taiwan can speak more than one language (e.g. Mandarin-Taiwanese, Mandarin-Taiwanese-Hakka, Mandarin-English, etc.), the real data from the survey is a bit more complex Basically, the data structure needed to draw an age-sex pyramid of language looks like:

Gender Ethnicity Age Group Avg. Fluency
female Mandarin 20-24 4.53
male Taiwanese 20-24 2.78
female Hakka 25-29 2.23
male Taiwanese 35-39 3.57

Preparation of Data

The raw data of Taiwan Language Survey can be retrieved here. The survey and raw data is in traditional Chinese. I’ll skip the step of cleaning raw data (e.g., turn variable names to English) and used the cleaned data survey.rds instead.

temp <- tempfile()
download.file('', destfile = temp)
data <- readr::read_rds(temp)
head(data, 3)
date curr_resid curr_resid_since settle_5yy home_town gender age kid_num edu_level work income work_hr tribe Mand_listen Mand_speak Tw_listen Tw_speak Hak_listen Hak_speak Ind_listen Ind_speak SEA_listen SEA_speak Eng_listen Eng_speak first_lang when_Mand when_Tw when_Hak when_Ind when_SEA when_Eng m_guard_identity f_guard_identity dad_Mand_speak dad_Tw_speak dad_Hak_speak dad_Eng_speak dad_Ind_speak dad_SEA_speak mom_Mand_speak mom_Tw_speak mom_Hak_speak mom_Eng_speak mom_Ind_speak mom_SEA_speak dad_mom_Mand_fq dad_mom_Tw_fq dad_mom_Hak_fq dad_mom_Ind_fq dad_mom_SEA_fq dad_mom_Other_fq me_dad_Mand_fq me_dad_Tw_fq me_dad_Hak_fq me_dad_Ind_fq me_dad_SEA_fq me_dad_Other_fq me_mom_Mand_fq me_mom_Tw_fq me_mom_Hak_fq me_mom_Ind_fq me_mom_SEA_fq me_mom_Other_fq
2018-06-12 106 1996 428 56 2 大學 金融業 10萬以上 45 - 50 小時 不具原住民身份 5 4 3 2 0 0 0 0 0 0 2 1 華語 7歲前 7歲前 未學會 未學會 未學會 12-15歲 父親 母親 3 0 0 0 0 0 3 4 0 0 0 0 幾乎全用 幾乎不用 幾乎不用 幾乎不用 幾乎不用 幾乎不用 幾乎全用 幾乎不用 幾乎不用 幾乎不用 幾乎不用 幾乎不用 約一半 多數使用 幾乎不用 幾乎不用 幾乎不用 幾乎不用
2018-06-13 204 2004 204 57 1 碩士 製造業 10萬以上 50 - 55 小時 不具原住民身份 5 5 5 5 0 0 0 0 0 0 4 4 華語 7歲前 7歲前 未學會 未學會 未學會 12-15歲 父親 母親 5 5 0 1 0 0 1 5 0 0 0 0 幾乎不用 多數使用 幾乎不用 幾乎不用 幾乎不用 幾乎不用 幾乎不用 多數使用 幾乎不用 幾乎不用 幾乎不用 幾乎不用 幾乎不用 多數使用 幾乎不用 幾乎不用 幾乎不用 幾乎不用
2018-06-13 103 2013 100 37 2 大學 金融業 55,000 - 60,000 25 - 30 小時 不具原住民身份 5 5 2 1 0 0 0 0 0 0 3 2 華語 7歲前 未學會 未學會 未學會 未學會 7-12歲 父親 母親 5 5 5 1 0 0 5 3 0 4 0 0 幾乎全用 少數使用 幾乎不用 幾乎不用 幾乎不用 幾乎不用 多數使用 少數使用 幾乎不用 幾乎不用 幾乎不用 幾乎不用 幾乎全用 幾乎不用 幾乎不用 幾乎不用 幾乎不用 幾乎不用

The survey data, data, contains 22 variables and 257 observations (subjects). I only need these variables below to create the plot I want:

  • gender: The gender of the subject
  • age: The age of the subject
  • m_guard_identity: Male guardian of the subject, should be ‘father’ in most cases
  • f_guard_identity: Female guardian of the subject, should be ‘mother’ in most cases
  • <lang>_speak: The subject’s fluency of a language. <lang> is one of ‘Mand’ (Mandarin), ‘Tw’ (Taiwanese), ‘Hak’ (Hakka), ‘Ind’ (languages of the indigenous peoples, aka Formosan languages, belong to Austronesian languages), ‘SEA’ (languages from South Easth Asia), and ‘Eng’ (English)
  • dad_<lang>_speak: The subject’s male guardian’s fluency of a language. <lang> is same as above.
  • mom_<lang>_speak: The subject’s female guardian’s fluency of a language. <lang> is same as above.
lang <- c('Mand', 'Tw', 'Hak', 'Ind', 'SEA', 'Eng')
cols <- c('gender', 'age', 'm_guard_identity', 'f_guard_identity',
          paste0(lang, '_speak'), 
          paste0('dad_', lang, '_speak'),
          paste0('mom_', lang, '_speak')
data <- data[, cols] %>%
    filter(gender == '男' | gender == '女')

Since some content of the survey data is in Chinese, the code below is used to translate it to English:

ch2eng <- function(x) {
  if (x == '男') return('Male')
  if (x == '女') return('Female')
  if (x == '母親') return('Mother')
  if (x == '父親') return('Father')
  if (x == '無') return('None')
  if (x == '(外)祖父') return('Grandpa')
  if (x == '(外)祖母') return('Grandma')
  if (x %in% c('阿姨', '嬸嬸', '舅媽', '姑姑', '伯母')) return('Aunt')
  if (x %in% c('叔叔', '伯伯', '舅舅', '姑丈', '姨丈')) return('Uncle')
  message('No translation found for `', x, '`', '\n')

ch2eng_vec <- function(vec) {
  new_vec <- vector(typeof(vec), length(vec))
  for (i in seq_along(vec)) {
    new_vec[[i]] <- ch2eng(vec[[i]])

data <- data %>% 
  mutate(gender = ch2eng_vec(gender),
         m_guard_identity = ch2eng_vec(m_guard_identity),
         f_guard_identity = ch2eng_vec(f_guard_identity))
gender age m_guard_identity f_guard_identity Mand_speak Tw_speak Hak_speak Ind_speak SEA_speak Eng_speak dad_Mand_speak dad_Tw_speak dad_Hak_speak dad_Ind_speak dad_SEA_speak dad_Eng_speak mom_Mand_speak mom_Tw_speak mom_Hak_speak mom_Ind_speak mom_SEA_speak mom_Eng_speak
Female 56 Father Mother 4 2 0 0 0 1 3 0 0 0 0 0 3 4 0 0 0 0
Female 57 Father Mother 5 5 0 0 0 4 5 5 0 0 0 1 1 5 0 0 0 0
Male 37 Father Mother 5 1 0 0 0 2 5 5 5 0 0 1 5 3 0 0 0 4
Female 44 Father Mother 5 4 0 0 0 4 5 5 0 0 0 0 5 5 0 0 0 0
Male 62 None None 5 5 0 0 0 1 5 0 4 0 0 0 5 5 0 0 0 0
Female 56 Father Mother 5 5 0 0 0 1 5 5 0 0 0 0 5 5 0 0 0 0

Defining Ethnicity

You might notice that there is no variable in the data which explicitly indicates the ethnicity of the subject. To serve the purpose of this survey – visualizing the vitality of languages in a community, ethnicity is defined solely by the linguistics competence of a subject’s guardians. To give a specific example, let subject A has a mother who can speak2 Mandarin and Taiwanese and a father who speaks Mandarin and Hakka, then subject A is categorized as a Mandarin, a Taiwanese, and a Hakka simultaneously.

The function filter_ethnic() is used to filter out subjects with specified ‘ethnicity’. This function is useful for drawing age-sex pyramid for each of the six languages.

filter_ethnic <- function(df, lang, lev = 3) {
    sp_lang <- vector("character", 3)
    sp_lang[1] <- paste0("dad_", lang, "_speak")
    sp_lang[2] <- paste0("mom_", lang, "_speak")
    sp_lang[3] <- paste0(lang, "_speak")
    df2 <- df %>% 
      filter(m_guard_identity != 'None' | f_guard_identity != 'None') %>%
      filter(.data[[sp_lang[1]]] >= lev | .data[[sp_lang[2]]] >= lev) %>%
      select(age, gender, sp_lang) 

Assinging Age Group to Subjects

Remember that the data structure needed for plotting age-sex pyramids requires age group to be the basic unit. data now consisits of single subjects, and we need information to group subjects together according to their ages. mutate_age_group() creates a new variable age_group by the subject’s age.

mutate_age_group <- function(df, range = 5){
    df$age_group <- as.character(
      cut(df$age, right = F, breaks = seq(10, 95, by = range))

cut() takes a numeric vector as its first input, and codes the values of the vector into new values according to the interval they fall into. In mutate_age_group(), cut() codes the input vectors according to the intervals specified in the argument breaks.

Now we can use these functions to add more information to the data frame. The idea is to first create a separated data frame for each ethnicity3 (by filter_ethnic()), then attact new variables to the data frame that indicate a subject’s ethnicity (ethn_group) and age group (age_group).

lang <- c('Mand', 'Tw', 'Hak', 'Ind', 'SEA', 'Eng')
lev <- c(3, 3, 3, 3, 3, 0)
ethn_list_df <- vector("list", length(lang))

for (i in seq_along(lang)){
  ethn_list_df[[i]] <- filter_ethnic(data, 
                                     lang = lang[i], 
                                     lev = lev[i]) %>%
    mutate(ethn_group = lang[i]) %>%
    mutate_age_group() %>%
    select(age, gender, age_group, ethn_group,
           paste0(lang[i], "_speak")) %>%
    rename(lang_fluency = paste0(lang[i], "_speak"))

Then we can recombine these data frames back to a single one, and this new data frame now has information about a subject’s ethinicity and age group he/she belongs to. (Note that the new data frame is expended since one subject can have several ethnicity, i.e. one subject can appear in different rows of the data frame with different ethnicity.)

bind_rows(ethn_list_df) %>% head()
age gender age_group ethn_group lang_fluency
56 Female [55,60) Mand 4
57 Female [55,60) Mand 5
37 Male [35,40) Mand 5
44 Female [40,45) Mand 5
56 Female [55,60) Mand 5
53 Male [50,55) Mand 5

Data for Plotting

Finally, we are ready to group the subjects together according to his/her gender, ethnicity, and age_group. After grouping, we can use dplyr::summarise() to calculate each group’s fluency of the language, which will be used as the variable on the horizontal axis of the age-sex pyramid.

pl_data <- bind_rows(ethn_list_df) %>%
  group_by(gender, ethn_group, age_group) %>%
  summarise(mean(lang_fluency)) %>%
  rename(avg_fluency = `mean(lang_fluency)`)

gender ethn_group age_group avg_fluency
Female Eng [15,20) 1.750000
Female Eng [20,25) 2.948718
Female Eng [25,30) 2.600000
Female Eng [30,35) 3.000000
Female Eng [35,40) 1.666667
Female Eng [40,45) 2.125000

Plotting Function

We are going to draw 6 age-sex pyramids, one for each languages. So instead of writing ggplot() six times, I wrote a plotting function:

pl_pyramid <- function(data, title = NULL) {
         aes(x = age_group,
             y = ifelse(gender == 'Male', avg_fluency, -avg_fluency),
             fill = gender)) +
  geom_bar(stat = "identity", width = 0.7) +
  scale_y_continuous(limits = c(-5, 5),
                     breaks = seq(-5, 5, 1),
                     labels = abs(seq(-5, 5, 1))) +
  coord_flip() +
  scale_fill_manual(values = c("#E41A1C", "#377EB8"),
                    breaks = c("Female", "Male")) +
  labs(x = "Age", y = "Fluency", fill = "", title = title)

Now we can start plotting. Let’s try Mand (Mandarin) and Hak (Hakka) first. We can use dplyr::filter() to filter out people speaking these languages:

tweak <-  theme_bw() + 
  theme(axis.text = element_text(size = 15),
        legend.justification = "right",
        legend.position = "bottom", = "vertical")

pl_data %>%
  filter(ethn_group == 'Mand') %>% 
  pl_pyramid(title = 'Mandarin') + tweak

pl_data %>%
  filter(ethn_group == 'Ind') %>% 
  pl_pyramid(title = 'Formosan Languages') + tweak

Wait! It seems quite strange. The age-sex pyramid of ‘Formosan Languages’ doesn’t look like a pyramid at all! This is because there were very few subjects defined as ‘indigenous people’ in the survey. To make plots like this (with only one or two age groups) comparable to others (such as that in ‘Mandarin’), we need one more function to insert missing age groups to the data frame so that ggplot can draw empty bars for us.

First, we need to find out all age groups in the data:

age_groups <- unique(pl_data$age_group)
 [1] "[15,20)" "[20,25)" "[25,30)" "[30,35)" "[35,40)" "[40,45)" "[45,50)"
 [8] "[50,55)" "[55,60)" "[60,65)" "[65,70)"

Then we can write a function fill_empty_age_group(), which takes a data frame as its first argument and checks whether there are age groups missing in the data frame (using its second argument, age_group_all, as comparison). If the age group is missing, fill_empty_age_group() appends a new row, which has age_group set to the missing age group and avg_fluency set to 0 (create an empty bar), to the input data frame.

fill_empty_age_group <- function(df, age_group_all) {
  for (i in seq_along(age_group_all)) {
    if (!(age_group_all[i] %in% df$age_group)) {
      df <- rbind(df, list(gender = "Female",
                           ethn_group = "doesnt_matter",
                           age_group = age_group_all[i],
                           avg_fluency = 0))

Now we’re ready to explore language loss in Taiwan.multiplot() is used to put multiple plots together. The source code of multiplot() is copied directly from Winston Chang’s Cookbook for R.

tweak2 <-  theme_bw() + 
  theme(legend.justification = "right",
        legend.position = "bottom", = "vertical")

py1 <- pl_data %>%
  filter(ethn_group == 'Mand') %>% 
  fill_empty_age_group(age_groups) %>%
  pl_pyramid(title = 'Mandarin') + tweak2

py2 <- pl_data %>%
  filter(ethn_group == 'Tw') %>%
  fill_empty_age_group(age_groups) %>%
  pl_pyramid(title = 'Taiwanese') + tweak2

py3 <- pl_data %>%
  filter(ethn_group == 'Hak') %>%
  fill_empty_age_group(age_groups) %>%
  pl_pyramid(title = 'Hakka') + tweak2

py4 <- pl_data %>%
  filter(ethn_group == 'Ind') %>%
  fill_empty_age_group(age_groups) %>%
  pl_pyramid(title = 'Formosan Languages') + tweak2

py5 <- pl_data %>%
  filter(ethn_group == 'SEA') %>%
  fill_empty_age_group(age_groups) %>%
  pl_pyramid(title = 'Languages of South East Asia') + tweak2

py6 <- pl_data %>%
  filter(ethn_group == 'Eng') %>%
  fill_empty_age_group(age_groups) %>%
  pl_pyramid(title = 'English') + tweak2

multiplot(py1, py2, py3, py4, py5, py6, cols = 2)

Language Loss in Taiwan

As described in Age-Sex Pyramid of Language above, we can learn about a language’s vitality in Taiwan from the age-sex pyramids drawn above.

For Formosan and South East Asian languages, there are too few data, and we can’t learn much about these languages from the plots. For Mandarin, the shape of the pyramid is retangular, indicating the linguistic competence of Mandarin is stable across people of all ages. This is expected as Mandarin, in Taiwan, is the most prevalent language, and most people use it as the primary language in workplace and home.

Pyramids of Taiwanese and Hakka have inverted trianglular shapes, indicating lower linguistic competence among yonger people. Indeed, it’s not uncommmon to see the fathers and mothers talk to their children in Mandarin but talk to their parents in Taiwanese. Taiwanese is the second prevalent language in Taiwan, but it is shrinking, particularly among young people. Hakka ranks third in prevalence. It faces similar situation to Taiwanese but the situation is even worse, since the usage of Hakka is mostly restricted to Hakka people whereas usage of Taiwanese is not restricted to particular groups of people (many indigenous and Hakka peoples speak fluent Taiwanese).

English has a special status in Taiwan. It is not a native tongue to people born and raised in Taiwan, but many people know at least a little English. This is because formal education and the (awareness of) globalization lead parents to place importance on English education of their children. This also gives the age-sex pyramid of English its appearance – a triangular shape with wider bottom than top. English is the only language that is growing in Taiwan and, arguably, the language with the strongest vitality. Table 1 summarizes the discussion above.

Table 1: Situations of the four most prevalent languages in Taiwan.
Vitality of Language Shape of Pyramid Examples (Taiwan)
Shrinking and Dying Inverted Triangle Taiwanese, Hakka
Growing Triangle English
Stable Retangualr Bar Mandarin
  1. The samples are not representative though, and it might only reflect the situation in Taipei (see the geographical distribution of the samples below). But I think there are still reasons to believe that other locations in Taiwan have similar phenomena.

    geo-distribution of samples

  2. Defined by scoring 3 or above in a self-reported 6-point scale measuring the fluency of a language spoken by the subject’s parents.

  3. For English, different from all other languages, the level used to determine ethnicity is 0, i.e. there is no filtering occuring, and all subjects are used. This is because, in Taiwan (and many other non-English speaking communities as well), English is not related to ethnicity and is strongly related to formal education and job requirements.

Last updated: 2019-02-17

To leave a comment for the author, please follow the link and comment on their blog: Yongfu's Blog. offers daily e-mail updates about R news and tutorials about learning R and many other topics. Click here if you're looking to post or find an R/data-science job.
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.

Never miss an update!
Subscribe to R-bloggers to receive
e-mails with the latest R posts.
(You will not see this message again.)

Click here to close (This popup will not appear again)